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

١٣ يوليو ٢٠٢٦ · آخر تحديث: ١٩ أكتوبر ٢٠٢٦

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

شارك المقال:

أكثر تحذير يراه متعلم React في حياته. في React 19 يظهر في الـ Console هكذا (نسخناه من تطبيق Next.js 16 بـ React 19.2):

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

Check the render method of `NoKey`. See https://react.dev/link/warning-keys for more information.

وفي React 18 وما قبله يبدأ بكلمة Warning:. السطر الثاني أهم ما فيه: Check the render method of NoKey يخبرك بالمكوّن الذي فيه الـ map المسؤول، فلا تبحث في المشروع كله.

والأخطر من التحذير نفسه هو «الحل» الشائع 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: «هذا نفس العنصر القديم» ويبقي حالته... فتظهر ملاحظتك داخل مهمة أخرى!

جرّبنا هذا فعليًا في React 19: ثلاث مهام لكل منها حقل <input>، كتبنا «اشترِ من المخبز القريب» في حقل المهمة الأولى «شراء خبز»، ثم حذفناها:

المفتاحما ظهر بعد حذف المهمة الأولى
key={index}«دفع الفاتورة» وفي حقلها: اشترِ من المخبز القريب
key={task.id}«دفع الفاتورة» وحقلها فارغ، كما يجب

مع index، حذف العنصر الأول جعل «دفع الفاتورة» تأخذ المفتاح 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>
))}

3) المفاتيح مكرّرة فعلًا: إن اخترت حقلًا ليس فريدًا (اسم، أو id يتكرّر بعد دمج قائمتين)، فالرسالة تختلف:

Encountered two children with the same key, `1`. Keys should be unique so that components
maintain their identity across updates. Non-unique keys may cause children to be duplicated
and/or omitted — the behavior is unsupported and could change in a future version.

القيمة بين علامتي الاقتباس (هنا 1) هي المفتاح المكرّر، فابحث عنها في بياناتك. وإن كانت القائمة مدموجة من مصدرين بمعرّفات متداخلة، فاجعل المفتاح مركّبًا: key={`${item.source}-${item.id}`}.

استخدام مفيد للمفتاح: إعادة ضبط مكوّن

بما أن تغيير key يجعل React يهدم المكوّن ويبنيه من جديد بحالة فارغة، يمكنك استخدامه عمدًا خارج القوائم. مثلًا نموذج تعديل مستخدم يجب أن يُفرَّغ حين تختار مستخدمًا آخر:

<EditProfileForm key={selectedUser.id} user={selectedUser} />

كل مستخدم جديد يعني مفتاحًا جديدًا، فتبدأ حالة النموذج من الصفر، بدل كتابة useEffect يراقب user ويصفّر كل حقل يدويًا.

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

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

الخلاصة

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

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

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

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

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

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

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

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

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

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

اقرأ أيضًا

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