تخطَّ إلى المحتوى

١٣ يوليو ٢٠٢٦

حل تحذير Each child should have a unique key في React

شارك المقال:

أكثر تحذير يراه متعلم React في حياته:

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

والأخطر منه: «الحل» الشائع key={index} الذي يُسكت التحذير ويزرع مكانه أخطاء صامتة أسوأ منه بكثير. لنفهم القصة مرة واحدة وللأبد.

لماذا يريد React مفتاحًا؟

عند كل إعادة رندر يقارن React قائمتك الجديدة بالقديمة ليلمس أقل قدر من DOM. المشكلة: كيف يعرف أن «العنصر الثاني الجديد» هو نفسه «الثالث القديم» انتقل مكانًا؟ المفتاح هو بطاقة الهوية:

{lessons.map((lesson) => (
  <LessonCard key={lesson.id} lesson={lesson} />
))}

بهوية ثابتة، الحذف من وسط القائمة يعني «انزع صاحب الهوية X» — بدون هدم وإعادة بناء كل ما بعده، ومع الحفاظ على حالته الداخلية (نص مكتوب، أكورديون مفتوح).

لماذا index قنبلة موقوتة؟

// 😨 يُسكت التحذير... ويبدّل الهويات عند أي تغيير
{items.map((item, index) => <Row key={index} item={item} />)}

المشهد الكارثي الكلاسيكي — قائمة مهام فيها حقول إدخال:

  1. ثلاث مهام بمفاتيح 0، 1، 2 — كتبت ملاحظة في حقل المهمة الأولى.
  2. حذفت المهمة الأولى.
  3. المهمة الثانية ورثت المفتاح 0 — فيقول React: «هذا نفس العنصر القديم» ويبقي حالته... فتظهر ملاحظتك داخل مهمة أخرى!

نفس السم مع الترتيب والإضافة بالمقدمة: القيم تلتصق بالمواضع لا بالبيانات. أخطاء لا تصرخ في الـ console — تظهر أمام المستخدم مباشرة.

المفتاح الصحيح — بالترتيب التنازلي

المصدرمثالالحكم
معرّف قاعدة البياناتitem.id✅ الذهب
حقل فريد طبيعيuser.email، post.slug✅ ممتاز
توليد عند الإنشاءcrypto.randomUUID() وقت إضافة العنصر✅ جيد للعناصر المحلية
Math.random() داخل الرندرمفتاح جديد كل رندر❌ كارثة: هدم وبناء دائم
indexالموضع⚠️ فقط للقوائم الجامدة (انظر الأسئلة)

نقطة التوليد المحلي مهمة: الـ UUID يُصنع عند إنشاء العنصر (في معالج الإضافة) ويُخزن معه — وليس داخل الرندر وإلا صار مفتاحًا جديدًا كل مرة، وهو أسوأ من index نفسه.

فخّا «عندي key والتحذير باقٍ»

1) المفتاح ليس على الابن المباشر لـ map:

// ❌ على الوسم الداخلي — React لا يراه حيث يبحث
{items.map((it) => (
  <div>
    <span key={it.id}>{it.name}</span>
  </div>
))}

// ✅ على أعلى عنصر معاد من الدالة
{items.map((it) => (
  <div key={it.id}>
    <span>{it.name}</span>
  </div>
))}

2) Fragment مختصر لا يقبل مفتاحًا:

// ❌ <>...</> لا يحمل خصائص
{items.map((it) => <><dt>{it.term}</dt><dd>{it.def}</dd></>)}

// ✅ الصيغة الكاملة
import { Fragment } from "react";
{items.map((it) => (
  <Fragment key={it.id}><dt>{it.term}</dt><dd>{it.def}</dd></Fragment>
))}

قائمة تحقّق سريعة

  • لكل map يرندر عناصر: key على الابن المباشر المعاد.
  • المصدر: id حقيقي ← حقل فريد ← UUID مولّد عند الإنشاء.
  • index فقط لقائمة جامدة بلا حالة داخلية — وعند الشك لا.
  • Math.random() بالرندر ممنوع منعًا قاطعًا.
  • التحذير باقٍ رغم key؟ → الابن المباشر، أو مفاتيح مكررة فعلًا، أو Fragment مختصر.

الخلاصة

المفتاح ليس «إرضاءً للتحذير» بل هوية يبني عليها React قراراته — أعطه هوية حقيقية ثابتة من بياناتك وستختفي عائلة كاملة من الأخطاء الغامضة: الحقول المختلطة، والحالات الملتصقة بالعنصر الخطأ، والرندر الزائد.

رسّخ الأساس في درس القوائم من مسار React، وأكمل عائلة أخطاء React مع حل حلقة useEffect اللانهائية وحل خطأ Hydration.

📚 مصادر رسمية للتعمّق: التوثيق الرسمي لـ React

الأسئلة الشائعة

لماذا يحتاج React لمفتاح أصلًا وما وظيفته؟

عند إعادة الرندر يقارن React القائمة الجديدة بالقديمة ليحدّث الحد الأدنى من DOM. بلا هوية ثابتة لكل عنصر لا يفرّق «عنصر حُذف» عن «كل ما بعده تغيّر» — المفتاح هو بطاقة الهوية التي تجعل المطابقة دقيقة، فيحرّك العنصر بدل هدمه وإعادة بنائه ويحافظ على حالته الداخلية.

التحذير يظهر مع أن عندي key — لماذا؟

أشهر سببين: وضعته على الوسم الداخلي بدل العنصر الأعلى المعاد مباشرة من map (يجب أن يحمله الابن المباشر — وإن كان Fragment استخدم الصيغة الطويلة <Fragment key=...>)، أو مفاتيحك مكررة فعلًا لأن الحقل الذي اخترته غير فريد (أسماء متشابهة مثلًا) — التحذير حينها unique key وليس missing.

متى يكون index مقبولًا كمفتاح؟

بشروط ثلاثة مجتمعة: القائمة ثابتة لا يُحذف منها ولا يُضاف، ولن يُعاد ترتيبها، وعناصرها بلا حالة داخلية (لا حقول إدخال ولا توسعة). قائمة عرض جامدة تحقق الشروط؟ index آمن حينها فعلًا — لكن أي شك، فمعرّف حقيقي.

اقرأ أيضًا

تصفّح كل المقالات في المدوّنة، أو ابدأ التعلّم من المسارات و خرائط الطريق.