Menu

רשימות ו-keys ב-React: map(), ה-prop key ו-index כ-key

כדי לרנדר רשימה ב-React קוראים ל-map() על מערך ומחזירים JSX לכל פריט, ונותנים לכל פריט prop בשם key. למדו filter ואחריו map, מאיפה keys צריכים להגיע, ולמה key מבוסס index מבלבל בין פריטים כשהרשימה משתנה.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

כדי לרנדר רשימה ב-React, קראו ל-map() על מערך והחזירו קטע JSX לכל פריט. תנו לאלמנט החיצוני של כל פריט prop בשם key עם ערך שמזהה את הפריט, בדרך כלל ה-ID שלו, כדי ש-React תוכל להבחין בין הפריטים כשהרשימה משתנה.

הוסיפו { id: 'dat', name: 'Date', price: 4 } למערך ותופיע שורה רביעית. ל-JSX אין תחביר לולאה משלו: map מחזיר מערך של אלמנטים, ו-React מרנדרת מערך על ידי רינדור כל אלמנט לפי הסדר.

רינדור מערכים עם map

map קורא לפונקציה שלכם פעם אחת לכל פריט ואוסף את התוצאות למערך חדש. בתוך סוגריים מסולסלים של JSX המערך הזה הוא סתם ערך, כך שאפשר לעשות map ישר בתוך ה-markup, כמו למעלה, או לבנות את המערך קודם:

const items = fruits.map((fruit) => <li key={fruit.id}>{fruit.name}</li>);
return <ul>{items}</ul>;

שני פרטים מכשילים אנשים. אם משתמשים בסוגריים מסולסלים בפונקציית החץ, צריך return: fruits.map((f) => { return <li key={f.id}>{f.name}</li>; }). בלעדיו הפונקציה מחזירה undefined והרשימה ריקה. ובנוסף, ה-key הולך על האלמנט שמוחזר מ-map, לא על אלמנט שבתוכו. אם הפריט הוא קומפוננטה, שימו את ה-key על הקומפוננטה: <FruitRow key={fruit.id} fruit={fruit} />.

filter, ואז map

כדי להציג חלק מהפריטים, סננו את המערך קודם ועשו map לתוצאה. כל מתודה עושה עבודה אחת, והשרשרת נקראת כמו המשפט "הפירות שמתאימים, כפריטי רשימה".

הקלידו er בתיבת החיפוש והרשימה מצטמצמת ל-Eraser, Ruler ו-Stapler; סמנו In stock only וגם Ruler נעלם. הרשימה המסוננת מחושבת בזמן הרינדור משני חלקי ה-state, כך שאין מערך שני ב-state שצריך לשמור מסונכרן. visible.length === 0 && הוא שימוש בטוח ב-&& כי ההשוואה היא בוליאני אמיתי (הדף על רינדור מותנה מראה מה קורה עם 0 חשוף).

מה keys עושים

כשרשימה מתרנדרת מחדש, React משווה את מערך האלמנטים החדש לקודם. keys הם הדרך שבה היא מתאימה ביניהם: האלמנט עם key 3 עכשיו הוא האלמנט עם key 3 מקודם, גם אם הוא זז. React שומרת את צומת ה-DOM של הפריט הזה ואת ה-state של הקומפוננטה שלו, ורק מזיזה או מעדכנת את מה שהשתנה. פריטים עם keys חדשים נוצרים, ו-keys שנעלמו מוסרים.

בלי keys, הדבר היחיד ש-React יכולה להתאים לפיו הוא המיקום. זה בסדר כל עוד שום דבר לא זז, ושגוי ברגע שמשהו זז.

למה index כ-key נשבר

map מעביר את ה-index כארגומנט השני שלו, ולכן key={index} מפתה. ה-index מתאר מיקום, לא פריט. הוסיפו פריט בראש הרשימה וה-index של כל פריט זז באחד, כך ש-React חושבת שפריט 0 הוא עדיין פריט 0 ושומרת שם את ה-state הישן שלו.

הדמו שמתחת מרנדר את אותה רשימה פעמיים, פעם עם key לפי index ופעם לפי ID. לכל שורה יש שדה קלט ששומר טקסט משלו.

הקלידו math ליד Ada בשתי הרשימות, ואז לחצו על Add to top. ברשימת ה-ID ההערה נשארת עם Ada. ברשימת ה-index ההערה קופצת לאדם החדש, כי שדה הקלט הראשון הוא עדיין השדה במיקום 0, ומיקום 0 הוא עכשיו מישהו אחר. השמות נכונים בשתי הרשימות, כי הם מגיעים מהנתונים; רק ה-state שחי ב-DOM ובקומפוננטות משתבש. אפליקציות אמיתיות נתקלות בזה עם checkboxes, פוקוס, אנימציות וכל קומפוננטה שמחזיקה useState.

key מבוסס index בטוח רק כשכל התנאים האלה מתקיימים: הרשימה אף פעם לא משנה סדר או מסוננת, פריטים אף פעם לא מוכנסים או מוסרים חוץ מבסוף, ולפריטים אין state משלהם. רשימה סטטית של קישורים בפוטר עומדת בזה. כל דבר שמשתמש יכול לערוך בדרך כלל לא.

מאיפה keys מגיעים

ה-key הכי טוב הוא מזהה שכבר נמצא בנתונים: מזהה ממסד נתונים, מק"ט של מוצר, slug, שם משתמש. לנתונים שמגיעים משרת כמעט תמיד יש אחד כזה.

לפריטים שנוצרים בדפדפן, תנו לכל פריט מזהה כשאתם יוצרים אותו ושמרו אותו עם הפריט: מונה, כמו בדמו שלמעלה, או crypto.randomUUID(). אף פעם אל תייצרו את ה-key בזמן הרינדור:

// Wrong: a new key on every render, so React remounts every item every time
{todos.map((todo) => <Todo key={Math.random()} todo={todo} />)}

// Right: the ID is created once, in the event that adds the item
function addTodo(text) {
    setTodos([...todos, { id: crypto.randomUUID(), text }]);
}

key שמשתנה בכל רינדור גרוע יותר מ-index: כל פריט נהרס ונוצר מחדש בכל פעם, שדות קלט מאבדים פוקוס תוך כדי הקלדה, ושום דבר לא שומר את ה-state שלו. אותו טריק שימושי כשעושים אותו בכוונה: שינוי ה-key של קומפוננטה מאפס אותה, והדף על useState משתמש בזה כדי לנקות טופס.

keys ייחודיים רק בין אחים

key צריך להיות ייחודי בתוך רשימה אחת, לא בכל האפליקציה. שתי רשימות נפרדות יכולות להשתמש שוב באותם מזהים, כי React משווה keys רק בין הילדים של אותו הורה.

בשני החלקים יש מנות עם keys 1 ו-2, וזה נכון: כל ul הוא רשימה משלו. keys כפולים בתוך רשימה אחת הם הבעיה: React לא יכולה להבחין בין הפריטים המתנגשים, וכשהרשימה מתעדכנת היא עלולה לשכפל או להשמיט אותם.

כשפריט אחד מרנדר כמה אלמנטים אחים, כמו <dt> ו-<dd>, עטפו אותם ב-<Fragment key={item.id}>. התחביר הקצר <> לא יכול לקבל key.

אזהרת ה-"unique key prop"

אם משמיטים את ה-key, React מדפיסה את זה בקונסול של הדפדפן בזמן פיתוח:

Each child in a list should have a unique "key" prop.

Check the render method of `App`.

השורה השנייה מציינת את הקומפוננטה שהרינדור שלה יצר את הרשימה, וכך יודעים איזה map לתקן. שני אחים עם אותו key מקבלים הודעה אחרת, שמתחילה ב-"Encountered two children with the same key". התצוגה המקדימה בדף הזה רצה כמו build של פרודקשן, שמדלג על הבדיקה הזו, אז לא תראו כאן את האזהרה גם אם תמחקו key. מחקו אחד בכל זאת בדוגמה הראשונה: הרשימה עדיין מתרנדרת, כי key חסר הוא סיכון לנכונות, לא קריסה. כדי לתקן את האזהרה, הוסיפו key יציב לאלמנט שמוחזר מ-map; הוספת key={index} משתיקה אותה אבל מחזירה את הבאג שמוצג למעלה.

שימו לב גם ש-key לא מועבר לקומפוננטה שלכם. בתוך FruitRow, props.key הוא undefined. אם הקומפוננטה צריכה את המזהה, העבירו אותו פעם שנייה כ-prop רגיל: <FruitRow key={f.id} id={f.id} />.

שאלות נפוצות

למה React צריכה key על פריטי רשימה?

keys אומרים ל-React איזה פריט הוא איזה בין רינדורים. כשהמערך משתנה, React מתאימה בין פריטים ישנים לחדשים לפי key, כך שהיא יכולה לשמור את ה-state וצומת ה-DOM של כל פריט עם הנתונים הנכונים במקום לנחש לפי מיקום.

אפשר להשתמש ב-index של המערך כ-key?

רק ברשימה שהסדר שלה אף פעם לא משתנה ושאף פעם לא מוסיפים או מסירים בה פריטים באמצע. אם פריטים יכולים לזוז, key מבוסס index קושר את ה-state למיקום, כך שטקסט שהוקלד, checkboxes ופוקוס מגיעים לפריט הלא נכון.

במה להשתמש בתור key?

במזהה ששייך לנתונים: מזהה ממסד נתונים, מק"ט של מוצר, שם משתמש. לפריטים שנוצרים בדפדפן, צרו מזהה פעם אחת כשיוצרים את הפריט (מונה או crypto.randomUUID()), אף פעם לא בזמן הרינדור.

האם keys צריכים להיות ייחודיים בכל האפליקציה?

לא. keys צריכים להיות ייחודיים רק בין אחים באותה רשימה. שתי רשימות שונות יכולות להשתמש באותם keys.

קומפוננטה יכולה לקרוא את ה-key של עצמה?

לא. key משמש את React ולא מועבר לקומפוננטה כ-prop. אם הקומפוננטה צריכה את המזהה, העבירו אותו שוב בשם אחר, כמו id={item.id}.

איך מתקנים את "Each child in a list should have a unique key prop"?

הוסיפו key לאלמנט החיצוני ביותר שמחזירים מ-map, עם מזהה יציב מהפריט. אם האלמנט הזה הוא fragment, השתמשו ב-<Fragment key={id}>, כי <> לא יכול לקבל key.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל