اگر زمانی را در React گذرانده باشید، حتماً با هشدار زرد رنگ در کنسول خود مواجه شده‌اید: “Each child in a list should have a unique ‘key’ prop.” این پیام شبیه یک پیشنهاد دوستانه به نظر می‌رسد، اما در واقع React دارد به شما هشدار می‌دهد که نمی‌تواند آیتم‌های لیست شما را از هم تشخیص دهد. اگر آن را نادیده بگیرید، در نهایت با باگی مواجه خواهید شد که بازسازی (reproduce) آن کلافه‌کننده است؛ مثلاً پرش state به ردیف اشتباه، از دست رفتن focus در inputهای متنی، یا اجرای انیمیشن‌ها روی المان اشتباه.

موتور رندرینگ React رابط کاربری شما را پیکسل به پیکسل مقایسه نمی‌کند. این موتور یک درخت سبک از اشیاء به نام virtual DOM می‌سازد، درخت جدید را با درخت قبلی مقایسه می‌کند و کوچک‌ترین مجموعه‌ی تغییرات لازم برای DOM واقعی را محاسبه می‌کند. وقتی یک لیست را رندر می‌کنید، React یک آرایه از المان‌های هم‌سطح (sibling) را می‌بیند. بدون کلیدها (keys)، راه مطمئنی وجود ندارد که بفهمد آیا یک آیتم جابه‌جا شده، جایگزین شده یا حذف شده است. به صورت پیش‌فرض، React سعی می‌کند آن‌ها را بر اساس موقعیت (position) مطابقت دهد که روشی شکننده است. کلیدها به عنوان شناسه‌های پایدار عمل می‌کنند. آن‌ها به React می‌گویند: «این المان همان المان قبلی است، حتی اگر اکنون در جای دیگری قرار گرفته باشد.» اگر این موضوع را اشتباه انجام دهید، به‌جای به‌روزرسانی‌های قطعی (deterministic)، با حدس و گمان روبرو خواهید شد.

حداقل اصلاح لازم

این هشدار معمولاً داخل یک فراخوانی map ظاهر می‌شود. شما باید یک مقدار منحصربه‌فرد را به ویژگی key در بالاترین سطح المانی که از iterator بازگردانده می‌شود، اختصاص دهید.

در اینجا الگویی را می‌بینید که در هر کد‌بیسِ دارای این هشدار، مشاهده می‌شود:

const UserList = ({ users }) => {
  return (
    <ul>
      {users.map((user) => (
        <li>{user.name}</li>
      ))}
    </ul>
  );
};

React سه تگ <li می‌بیند و هیچ ایده‌ای ندارد که کدام مربوط به کدام است. اصلاح آن تنها با یک ویژگی (attribute) انجام می‌شود:

const UserList = ({ users }) => {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

مقدار key باید مستقیماً به المانی که داخل callbackِ تابع map قرار دارد اختصاص یابد. اگر <li را به یک کامپوننت جداگانه به نام UserItem استخراج کنید، key همچنان باید در محل فراخوانیِ آن کامپوننت قرار بگیرد:

{users.map((user) => (
  <UserItem key={user.id} user={user} />
))}

قرار دادن key در داخل UserItem روی تگ <div> داخلی آن، باعث خاموش شدن هشدار نمی‌شود و رفتار reconciliation را نیز اصلاح نمی‌کند. React به دنبال key در المانی می‌گردد که توسط iterator بازگردانده شده است.

چرا استفاده از index به عنوان key خطرناک است

وسوسه‌انگیز است که با استفاده از آرگومان دوم تابع map این هشدار را ساکت کنید:

{users.map((user, index) => (
  <li key={index}>{user.name}</li>
))}

این کار نویز کنسول را از بین می‌برد، اما مشکل اصلی را حل نمی‌کند. ایندکس‌های آرایه، هویت (identity) نیستند؛ آن‌ها صرفاً موقعیت هستند و موقعیت‌ها تغییر می‌کنند.

لیستی از سه کاربر را تصور کنید که با این ترتیب رندر شده‌اند:

  1. Alice (index 0)
  2. Bob (index 1)
  3. Charlie (index 2)

اگر آلیس را حذف کنید، باب به index 0 و چارلی به index 1 منتقل می‌شوند. React درخت جدید را با درخت قدیمی مقایسه می‌کند. می‌بیند که index 0 اکنون داده‌های باب را نگه می‌دارد، بنابراین همان node در DOM را که قبلاً آلیس را نشان می‌داد، تغییر می‌دهد (mutate می‌کند). اگر آن node دارای focus بود، مکان‌نما (cursor) در ردیف اول باقی می‌ماند، اما متن به «باب» تغییر می‌کند. اگر آن ردیف شامل یک <input> با state محلی بود، آن state همچنان به index 0 چسبیده باقی می‌ماند. کاربر در چیزی که شبیه ردیف باب به نظر می‌رسد تایپ می‌کند، اما state متعلق به آلیس بود. همین آشفتگی هنگام مرتب‌سازی (sort)، فیلتر کردن یا اضافه کردن آیتم‌ها به ابتدای لیست رخ می‌دهد. تنها جای امن برای استفاده از index به عنوان key، لیستی است که واقعاً ایستا (static) باشد: بدون تغییر ترتیب، بدون فیلتر کردن، بدون درج و بدون حذف. لینک‌های ناوبری (navigation links) که به صورت hardcoded هستند و هرگز تغییر نمی‌کنند، یک مثال خوب هستند. هر چیز دیگری به یک شناسه واقعی نیاز دارد.

کجا یک key پایدار پیدا کنیم

بهترین key، یک شناسه منحصربه‌فرد است که از قبل در مدل داده‌ای شما وجود دارد. کلیدهای اصلی (primary keys) پایگاه داده مانند id ایده‌آل هستند، زیرا منحصر‌به‌فرد بودن آن‌ها تضمین شده است و در طول رندرها باقی می‌مانند. اگر backend شما اشیائی با uuid ،slug یا هر فیلد منحصربه‌فرد طبیعی دیگری برمی‌گرداند، از آن‌ها استفاده کنید.

وقتی پاسخ API شما فاقد هرگونه فیلد منحصربه‌فرد است، دو راه عملی دارید. اول، با تیم backend خود صحبت کنید و از آن‌ها بخواهید یک id اضافه کنند. ارسال داده‌های رابطه‌ای (relational data) بدون کلید اصلی، یک نشانه‌ی بد (smell) در طراحی است و اصلاح آن در منبع، ابهام را در تمام بخش‌های stack شما از بین می‌برد. دوم، اگر آیتم‌ها را کاملاً در سمت client تولید می‌کنید — مثلاً یک لیست انجام کار (todo list) که در آن کاربران وظایف را قبل از ارسال به سرور ایجاد می‌کنند — یک ID را فقط یک بار در زمان ایجاد تولید کنید. کتابخانه‌هایی مانند uuid یا nanoid دقیقاً برای همین کار ساخته شده‌اند. وقتی کاربر فرم را ارسال می‌کند، ID را تولید کنید، آن را در شیء ذخیره کنید و برای همیشه از آن به عنوان key استفاده کنید.

هرگز یک key را در مسیر رندر (render path) تولید نکنید. فراخوانی Math.random() یا Date.now() در طول رندر یک کامپوننت، در هر بار اجرا مقدار جدیدی تولید می‌کند. React یک key جدید می‌بیند، فرض می‌کند که این یک المان کاملاً جدید است، node قدیمی در DOM را از بین می‌برد و یک node جدید می‌سازد. هر state‌ای که داخل آن المان باشد، ریست می‌شود. focus از دست می‌رود. عملکرد (performance) به شدت افت می‌کند زیرا React در حال انجام کارهای غیرضروری روی DOM است. یک key که به صورت تصادفی تولید شده، حتی از نداشتن key هم بدتر است.

Fragmentها، کامپوننت‌ها و Scope

یک تله‌ی کمتر بدیهی، مربوط به React Fragments است. اگر روی داده‌ها پیمایش (map) می‌کنید و نیاز دارید چندین عنصر هم‌سطح (sibling) را بدون یک نگهدارنده‌ی <div> برگردانید، ممکن است به سراغ نحو (syntax) کوتاه بروید:

{items.map((item) => (
  <>
    <dt>{item.term}</dt>
    <dd>{item.definition}</dd>
  </>
))}

نحو کوتاه‌شده‌ی <>...</> از props پشتیبانی نمی‌کند، به این معنی که نمی‌توانید یک key به آن اختصاص دهید. در این حالت، از نحو کامل و صریح استفاده کنید:

{items.map((item) => (
  <React.Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.definition}</dd>
  </React.Fragment>
))}

React به آن key در Fragment نیاز دارد تا بتواند آن جفت را به عنوان یک واحد واحد در طول رندرها دنبال کند.

نکته‌ی ظریف دیگر این است که keyها به معنای معمول، prop نیستند. اگر بنویسید <ListItem key={item.id} /> ، کامپوننت ListItem نمی‌تواند props.key را بخواند. React آن را به صورت داخلی برای مدیریت داخلی مصرف می‌کند. اگر کامپوننت شما واقعاً برای منطق خود به آن شناسه نیاز دارد، آن را جداگانه با نام دیگری مانند itemId ارسال کنید.

قوانین کاربردی برای حفظ امنیت کد شما

  • استفاده از IDهای پایگاه داده را ترجیح دهید. آن‌ها منحصربه‌فرد، عددی یا رشته‌ای و پایدار هستند.
  • برای داده‌های صرفاً کلاینتی از uuid یا nanoid استفاده کنید. شناسه را فقط یک بار هنگام ایجاد رکورد تولید کنید، نه در داخل رندرِ کامپوننت.
  • اگر لیست تغییر می‌کند، هرگز key را از ایندکس آرایه استخراج نکنید. مرتب‌سازی، فیلتر کردن و حذف کردن باعث بروز باگ‌های بصری و باگ‌های مربوط به state می‌شود.
  • هرگز از Math.random()، Date.now() یا هر مقداری که بین رندرها تغییر می‌کند استفاده نکنید. این کار باعث unmounting و remounting غیرضروری می‌شود.
  • به یاد داشته باشید که Fragments اگر داخل یک map قرار دارند و به key نیاز دارند، باید از نحو کامل استفاده کنند.
  • key را روی عنصری که توسط map برگردانده می‌شود قرار دهید، نه داخل یک کامپوننت فرزند.

نتیجه‌گیری اصلی

propِ key یک قانون تزئینی برای lint نیست. این روشی است که React هویت را در طول رندرها حفظ می‌کند. آن را مانند یک کلید اصلی (primary key) در یک جدول پایگاه داده تصور کنید. وقتی آن هویت پایدار باشد، React می‌تواند عناصر را با دقت جابه‌جا، به‌روزرسانی و حذف کند. وقتی این هویت وجود نداشته باشد یا ناپایدار باشد، بهای آن را با وضعیتِ UI خراب و فرآیند reconciliation کند پرداخت خواهید کرد. آن را یک‌بار در لایه داده اصلاح کنید، و لیست‌های شما بدون توجه به میزان رشد یا تغییرشان، به شکلی قابل پیش‌بینی رفتار خواهند کرد.