לדלג לתוכן

רשימות ומפתחות בריאקט - מה זה key

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

הצגת רשימה עם map

function ProductList({ products }) {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

map הופך כל איבר במערך לאלמנט JSX, ו-key הוא prop מיוחד שריאקט משתמש בו מאחורי הקלעים - הוא לא מגיע לדפדפן, ולא ניתן לגשת אליו כמו ל-props רגילים בתוך הקומפוננטה.

למה בכלל צריך key

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

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

למה index כ-key זה בעייתי

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

{products.map((product, index) => (
  <li key={index}>{product.name}</li>
))}

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

הנה דוגמה שממחישה את הבעיה - רשימת שדות טקסט לעריכה:

function EditableList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>
          <input defaultValue={item.text} />
        </li>
      ))}
    </ul>
  );
}

אם מוחקים את האיבר הראשון ברשימה, האינדקסים של כל האיברים שאחריו זזים אחורה - מה שהיה index 1 הופך ל-0, וכן הלאה. ריאקט, שמזהה אלמנטים לפי key, חושב שה-key=0 הוא עדיין אותו אלמנט כמו קודם, ולכן הוא לא בהכרח יוצר מחדש את שדה ה-input. התוצאה - ה-DOM node של השדה נשאר, אבל עם הערך הפנימי (defaultValue) של הפריט שנמחק, לא של הפריט החדש שתפס את המקום שלו. זה בדיוק הסוג של באג ש"נראה אקראי" ולוקח זמן להבין למה הוא קורה.

הפתרון - key יציב וייחודי

הפתרון הנכון הוא להשתמש במזהה ייחודי ויציב שקשור לנתונים עצמם, לא למיקום שלהם ברשימה - בדרך כלל id שמגיע מהשרת או ממסד הנתונים:

{products.map((product) => (
  <li key={product.id}>{product.name}</li>
))}

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

מה לעשות כשאין id טבעי

לפעמים הנתונים באמת לא מגיעים עם מזהה ייחודי, במיוחד ברשימות שנוצרות מקומית באפליקציה. במקרה כזה, הפתרון הכי טוב הוא ליצור מזהה ייחודי ברגע יצירת האיבר, ולא ברגע הרינדור:

function addItem(text) {
  const newItem = { id: crypto.randomUUID(), text };
  setItems([...items, newItem]);
}

crypto.randomUUID() היא פונקציה מובנית בדפדפן שיוצרת מזהה ייחודי אקראי. חשוב - אל תיצרו את המזהה בתוך פונקציית ה-map עצמה, כי אז הוא ייווצר מחדש בכל רינדור, וישבור את כל התועלת של key יציב.

מתי index כ-key באמת בסדר

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

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

לסיכום

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

יש לכם באג מוזר ברשימה שנראה כמו "state שנדבק לפריט הלא נכון"? זה כנראה בדיוק זה. תשתפו בדיסקורד.

הצטרפו לקהילה בדיסקורד