إذا كنت قد قضيت أي وقت في العمل مع React، فلا بد أنك رأيت التحذير الأصفر في وحدة التحكم (console): "Each child in a list should have a unique ‘key’ prop." قد يبدو الأمر وكأنه مجرد اقتراح مهذب، لكن React في الواقع يحذرك من أنه لا يستطيع التمييز بين عناصر القائمة الخاصة بك. إذا تجاهلت هذا التحذير، فستنتهي بك المطاف بإرسال برمجية تحتوي على خطأ (bug) يصعب تتبعه وإعادة إنتاجه—مثل قفز الحالة (state) إلى الصف الخطأ، أو فقدان حقول النص للتركيز (focus)، أو تشغيل الرسوم المتحركة على العنصر الخطأ.

محرك الرندرة (rendering engine) في React لا يقارن واجهة المستخدم الخاصة بك بكسل بكسل. بدلاً من ذلك، يقوم ببناء شجرة خفيفة من الكائنات تسمى الـ virtual DOM، ويقارن الشجرة الجديدة بالشجرة السابقة، ثم يحسب أصغر مجموعة من التغييرات المطلوبة لتحديث الـ DOM الحقيقي. عندما تقوم برندرة قائمة، يرى React مصفوفة من العناصر المتجاورة. وبدون مفاتيح (keys)، لا يملك وسيلة موثوقة لمعرفة ما إذا كان العنصر قد انتقل، أو تم استبداله، أو تمت إزالته. لذا، فإنه يعتمد افتراضيًا على المطابقة حسب الموقع، وهو أمر هش. تعمل المفاتيح كمعرفات مستقرة؛ فهي تخبر React: "هذا العنصر هو نفسه الذي كان موجودًا من قبل، حتى لو كان الآن في مكان مختلف". إذا أخطأت في هذا الأمر، فستستبدل التحديثات الحتمية (deterministic updates) بالتخمين.

الإصلاح الأدنى

عادة ما يظهر التحذير داخل استدعاء map. يجب عليك تعيين قيمة فريدة لخاصية key على العنصر في المستوى الأعلى الذي تعيده عملية التكرار (iterator).

إليك النمط الذي تراه في كل كود برمجي يتسبب في ظهور هذا التحذير:

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

يرى React ثلاثة وسوم <li وليس لديه أي فكرة عن أي منها هو أي واحد. التصحيح يتطلب خاصية واحدة فقط:

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

يجب تعيين الـ key للعنصر الموجود مباشرة داخل دالة الاستدعاء (callback) الخاصة بـ map. إذا قمت باستخراج الـ <li إلى مكون منفصل مثل UserItem ، فإن المفتاح لا يزال يجب أن يوضع على المكون في مكان الاستدعاء:

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

وضع المفتاح داخل UserItem على عنصر <div> الداخلي لن يؤدي إلى إسكات التحذير ولن يصلح سلوك عملية المطابقة (reconciliation). يبحث React عن المفتاح في العنصر الذي تعيده عملية التكرار.

لماذا يُعد استخدام الفهرس (Index) كمفتاح أمرًا خطيرًا

من المغري إسكات التحذير عبر استخدام الوسيط الثاني لدالة map:

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

هذا يزيل الضجيج من وحدة التحكم، لكنه لا يحل المشكلة الأساسية. ففهارس المصفوفات (Array indices) ليست معرفات (identities)؛ بل هي مجرد مواقع، والمواقع تتغير.

تخيل قائمة من ثلاثة مستخدمين يتم عرضها بهذا الترتيب:

  1. Alice (الفهرس 0)
  2. Bob (الفهرس 1)
  3. Charlie (الفهرس 2)

إذا حذفت Alice، سينتقل Bob إلى الفهرس 0 وينتقل Charlie إلى الفهرس 1. يقارن React الشجرة الجديدة بالشجرة القديمة، ويرى أن الفهرس 0 يحتوي الآن على بيانات Bob، لذا يقوم بتعديل عقدة الـ DOM الموجودة التي كانت تعرض Alice سابقًا. إذا كان هذا العنصر يمتلك التركيز (focus)، فسيظل المؤشر في الصف الأول، ولكن النص سيتغير إلى Bob. إذا كان الصف يحتوي على <input> بحالة محلية (local state)، فستظل تلك الحالة عالقة في الفهرس 0. سيكتب المستخدم في ما يبدو أنه صف Bob، لكن الحالة تعود لـ Alice. يحدث نفس الفوضى عند فرز العناصر، أو تصفيتها، أو إضافة عناصر في البداية. المكان الوحيد الآمن لاستخدام الفهرس كمفتاح هو القائمة الثابتة حقًا: التي لا يتم إعادة ترتيبها، ولا تصفيتها، ولا يتم إدراج عناصر فيها، ولا حذف عناصر منها. روابط التنقل الثابتة (hardcoded) التي لا تتغير أبدًا هي مثال جيد. أما أي شيء آخر فيحتاج إلى معرف حقيقي.

أين تجد مفتاحًا مستقرًا

أفضل مفتاح هو المعرف الفريد الموجود بالفعل في نموذج البيانات الخاص بك. المفاتيح الأساسية (primary keys) في قواعد البيانات مثل id هي مثالية لأنها مضمونة الفرادة وتظل ثابتة عبر عمليات الرندرة. إذا كانت واجهة برمجة التطبيقات (API) الخاصة بك تعيد كائنات تحتوي على uuid أو slug أو أي حقل فريد بطبيعته، فاستخدم ذلك بدلاً من ذلك.

عندما تفتقر استجابة الـ API الخاصة بك إلى أي حقل فريد، فلديك مساران عمليان. أولاً، تحدث فريق الخلفية (backend) واطلب منهم تضمين id. إن إرسال بيانات علاقية بدون مفتاح أساسي يعد ممارسة سيئة (code smell)، وإصلاح ذلك من المصدر يزيل الغموض في كامل بنيتك البرمجية. ثانيًا، إذا كنت تقوم بإنشاء العناصر بالكامل من جانب العميل (client side)—على سبيل المثال، قائمة مهام حيث ينشئ المستخدمون المهام قبل إرسالها إلى الخادم—فقم بإنشاء معرف (ID) مرة واحدة عند وقت الإنشاء. مكتبات مثل uuid أو nanoid صُممت خصيصًا لهذا الغرض. قم بإنشاء المعرف عندما يرسل المستخدم النموذج، وخزنه في الكائن، واستخدمه كمفتاح للأبد.

لا تقم أبدًا بإنشاء مفتاح داخل مسار الرندرة (render path). استدعاء Math.random() أو Date.now() أثناء رندرة المكون ينتج قيمة جديدة في كل تمريرة. يرى React مفتاحًا جديدًا، فيفترض أنه عنصر جديد تمامًا، فيقوم بتدمير عقدة الـ DOM القديمة وإنشاء واحدة جديدة. أي حالة داخل ذلك العنصر ستتم إعادة ضبطها، وسيُفقد التركيز (focus)، وسينخفض الأداء لأن React يقوم بعمليات DOM غير ضرورية. المفتاح الذي يتم إنشاؤه عشوائيًا أسوأ من عدم وجود مفتاح على الإطلاق.

Fragments، والمكونات (Components)، والنطاق (Scope)

ثمة فخ أقل وضوحاً يتعلق بـ React Fragments. إذا كنت تقوم بعمل map على البيانات وتحتاج إلى إرجاع عناصر متجاورة متعددة دون استخدام عنصر تغليف <div> ، فقد تلجأ إلى الصيغة المختصرة:

{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 حتى يتمكن من تتبع الزوج كوحدة واحدة عبر عمليات الـ renders.

نقطة دقيقة أخرى: الـ keys ليست props بالمعنى المعتاد. إذا كتبت <ListItem key={item.id} /> ، فلن يتمكن مكون ListItem من قراءة props.key. يقوم React باستهلاكها داخلياً لأغراض التتبع الداخلي. إذا كان مكونك يحتاج حقاً إلى المعرف (identifier) لمنطقه الخاص، فقم بتمريره بشكل منفصل تحت اسم مختلف، مثل itemId.

قواعد عملية لضمان سلامة الكود الخاص بك

  • فضل استخدام معرفات قاعدة البيانات (database IDs). فهي فريدة، سواء كانت رقمية أو نصية، وتتميز بالاستقرار.
  • استخدم uuid أو nanoid للبيانات الخاصة بالعميل فقط (client-only data). قم بإنشاء المعرف مرة واحدة عند إنشاء السجل، وليس داخل عملية الـ render للمكون.
  • لا تستمد الـ key أبداً من فهرس المصفوفة (array index) إذا كانت القائمة قابلة للتغيير. فعمليات الفرز (sorting)، والتصفية (filtering)، والحذف ستؤدي إلى أخطاء في العرض (visual) وفي الحالة (state).
  • لا تستخدم أبداً Math.random() أو Date.now() أو أي قيمة تتغير بين عمليات الـ renders. فهذا يفرض عمليات unmounting و remounting غير ضرورية.
  • تذكر أن الـ Fragments تحتاج إلى الصيغة الطويلة إذا كانت موجودة داخل map وتتطلب key.
  • ضع الـ key على العنصر الذي تعيده دالة map ، وليس داخل مكون فرعي.

الخلاصة الحقيقية

خاصية الـ key ليست مجرد قاعدة تنسيقية (lint rule). إنها الطريقة التي يحافظ بها React على الهوية عبر عمليات الـ renders. فكر فيها مثل المفتاح الأساسي (primary key) في جدول قاعدة بيانات. عندما تكون هذه الهوية مستقرة، يمكن لـ React نقل العناصر وتحديثها وإزالتها بدقة. أما عندما تكون مفقودة أو غير مستقرة، فستدفع الثمن من خلال حالة واجهة مستخدم (UI state) تالفة وعملية مطابقة (reconciliation) بطيئة. قم بإصلاح الأمر مرة واحدة في طبقة البيانات (data layer)، وستعمل قوائمك بشكل متوقع مهما كبر حجمها أو تغيرت.