لعرض قائمة في React، استدعِ map() على مصفوفة وأرجع قطعة من JSX لكل عنصر. أعطِ العنصر الخارجي لكل عنصر الـ prop المسماة key بقيمة تعرّف ذلك العنصر، عادة معرّفه، لتتمكن React من التمييز بين العناصر عندما تتغير القائمة.
أضف { id: 'dat', name: 'Date', price: 4 } إلى المصفوفة فيظهر سطر رابع. ليس لـ JSX صيغة حلقات خاصة بها: يُرجع map مصفوفة من العناصر، وتعرض React المصفوفة بعرض كل عنصر فيها بالترتيب.
عرض المصفوفات بـ map
يستدعي map دالتك مرة لكل عنصر ويجمع النتائج في مصفوفة جديدة. داخل أقواس JSX المعقوصة تكون تلك المصفوفة مجرد قيمة، فيمكنك المرور عليها مباشرة في الترميز، كما في الأعلى، أو بناء المصفوفة أولًا:
const items = fruits.map((fruit) => <li key={fruit.id}>{fruit.name}</li>);
return <ul>{items}</ul>;
تفصيلتان توقعان الناس في الخطأ. إذا استخدمت أقواسًا معقوصة في الدالة السهمية، فأنت تحتاج إلى return: fruits.map((f) => { return <li key={f.id}>{f.name}</li>; }). من دونها تُرجع الدالة undefined وتكون القائمة فارغة. والمفتاح key يوضع على العنصر المُرجع من map، لا على عنصر بداخله. إذا كان العنصر مكوّنًا، فضع المفتاح على المكوّن: <FruitRow key={fruit.id} fruit={fruit} />.
رشّح، ثم مرّ على العناصر
لعرض بعض العناصر، رشّح المصفوفة أولًا ثم مرّ على النتيجة. كل تابع يؤدي مهمة واحدة، وتُقرأ السلسلة كجملة «الفواكه المطابقة، كعناصر قائمة».
اكتب er في مربع البحث فتضيق القائمة إلى Eraser وRuler وStapler؛ وحدّد In stock only فيختفي Ruler أيضًا. تُحسب القائمة المرشحة أثناء العرض من قطعتي الحالة، فلا توجد مصفوفة ثانية في الحالة عليك إبقاؤها متزامنة. وvisible.length === 0 && استخدام آمن لـ && لأن المقارنة قيمة منطقية حقيقية (تُظهر صفحة العرض الشرطي ما يحدث مع 0 مجردة).
ما الذي تفعله المفاتيح
عندما تُعاد عرض قائمة، تقارن React مصفوفة العناصر الجديدة بالسابقة. المفاتيح هي وسيلة مطابقتها: العنصر ذو المفتاح 3 الآن هو العنصر ذو المفتاح 3 من قبل، حتى لو تحرّك. تحتفظ React بعقدة DOM لذلك العنصر وبحالة مكوّنه، ولا تنقل أو تحدّث إلا ما تغيّر. العناصر ذات المفاتيح الجديدة تُنشأ، والمفاتيح التي اختفت تُزال.
من دون مفاتيح، لا تستطيع React المطابقة إلا بالموضع. هذا مقبول ما دام لا شيء يتحرك، وخاطئ بمجرد أن يتحرك شيء.
لماذا تنكسر مفاتيح الفهرس
يمرر map الفهرس كوسيطه الثاني، فيكون key={index} مغريًا. الفهرس يصف موضعًا لا عنصرًا. أدرج عنصرًا في الأعلى فيتزحزح فهرس كل عنصر بمقدار واحد، فتظن React أن العنصر 0 ما زال العنصر 0 وتُبقي حالته القديمة هناك.
يعرض المثال أدناه القائمة نفسها مرتين، مرة بمفتاح الفهرس ومرة بالمعرّف. لكل صف حقل إدخال يحتفظ بنصه الخاص.
اكتب math بجانب Ada في القائمتين، ثم انقر Add to top. في قائمة المعرّف تبقى الملاحظة مع Ada. أما في قائمة الفهرس فتقفز الملاحظة إلى الشخص الجديد، لأن الحقل الأول ما زال الحقل في الموضع 0، والموضع 0 أصبح لشخص آخر. الأسماء صحيحة في القائمتين، لأنها تأتي من البيانات؛ وما يخطئ هو فقط الحالة التي تعيش في DOM وفي المكوّنات. تصطدم التطبيقات الحقيقية بهذا مع مربعات الاختيار والتركيز والحركات وأي مكوّن يحمل useState.
مفاتيح الفهرس آمنة فقط عندما تتحقق كل هذه الشروط: لا يُعاد ترتيب القائمة ولا تُرشح أبدًا، ولا تُدرج عناصر أو تُحذف إلا في النهاية، وليس للعناصر حالة خاصة بها. قائمة ثابتة من روابط التذييل تستوفي ذلك. أما ما يستطيع المستخدم تعديله فعادة لا.
من أين تأتي المفاتيح
أفضل مفتاح هو معرّف موجود أصلًا في البيانات: معرّف قاعدة البيانات، أو رمز المنتج، أو المعرّف النصي في الرابط، أو اسم المستخدم. البيانات الآتية من خادم تحمل واحدًا منها دائمًا تقريبًا.
وللعناصر المنشأة في المتصفح، أعطِ كل عنصر معرّفًا عند إنشائه وخزّنه معه: عدّادًا، كما في المثال أعلاه، أو crypto.randomUUID(). ولا تولّد المفتاح أبدًا أثناء العرض:
// Wrong: a new key on every render, so React remounts every item every time
{todos.map((todo) => <Todo key={Math.random()} todo={todo} />)}
// Right: the ID is created once, in the event that adds the item
function addTodo(text) {
setTodos([...todos, { id: crypto.randomUUID(), text }]);
}
المفتاح الذي يتغير في كل عرض أسوأ من الفهرس: يُدمَّر كل عنصر ويُعاد إنشاؤه في كل مرة، وتفقد حقول الإدخال التركيز أثناء الكتابة، ولا يحتفظ أي شيء بحالته. والحيلة نفسها مفيدة عن قصد: تغيير مفتاح مكوّن يعيد ضبطه، وتستخدم صفحة useState ذلك لتفريغ نموذج.
المفاتيح فريدة بين الأشقاء فقط
يجب أن يكون المفتاح فريدًا داخل قائمة واحدة، لا في التطبيق كله. يمكن لقائمتين منفصلتين إعادة استخدام المعرّفات نفسها، لأن React لا تقارن المفاتيح إلا بين أبناء الأب نفسه.
في القسمين أطباق بالمفتاحين 1 و2، وهذا صحيح: كل ul قائمة مستقلة. المفاتيح المكررة داخل قائمة واحدة هي المشكلة: لا تستطيع React التمييز بين العناصر المتعارضة، وعندما تتحدث القائمة قد تكررها أو تُسقطها.
عندما يعرض عنصر واحد عدة عناصر شقيقة، مثل <dt> و<dd>، فغلّفها في <Fragment key={item.id}>. الصيغة المختصرة <> لا تقبل مفتاحًا.
تحذير «unique key prop»
إذا أغفلت المفتاح، تطبع React هذا في وحدة تحكم المتصفح أثناء التطوير:
Each child in a list should have a unique "key" prop.
Check the render method of `App`.
يسمّي السطر الثاني المكوّن الذي أنتج عرضُه القائمة، فيخبرك أي map عليك إصلاحه. وشقيقان بالمفتاح نفسه يحصلان على رسالة مختلفة تبدأ بـ "Encountered two children with the same key". تعمل المعاينة في هذه الصفحة مثل بناء الإنتاج الذي يتخطى هذا الفحص، فلن ترى التحذير هنا حتى لو حذفت key. احذف واحدًا على أي حال في المثال الأول: ما زالت القائمة تُعرض، لأن المفتاح المفقود خطر على الصحة لا سبب للانهيار. لإصلاح التحذير، أضف مفتاحًا ثابتًا إلى العنصر المُرجع من map؛ أما إضافة key={index} فتُسكت التحذير لكنها تعيد الخطأ الموضح أعلاه.
لاحظ أيضًا أن key لا تُمرَّر إلى مكوّنك. داخل FruitRow تكون props.key هي undefined. إذا احتاج المكوّن إلى المعرّف، فمرّره مرة ثانية كـ prop عادية: <FruitRow key={f.id} id={f.id} />.
الأسئلة الشائعة
لماذا تحتاج React إلى مفتاح على عناصر القائمة؟
تخبر المفاتيح React أي عنصر هو أي عنصر بين مرات العرض. عندما تتغير المصفوفة تطابق React العناصر القديمة والجديدة بالمفتاح، فتستطيع إبقاء حالة كل عنصر وعقدته في DOM مع البيانات الصحيحة بدلًا من التخمين حسب الموضع.
هل يمكنني استخدام فهرس المصفوفة كمفتاح؟
فقط لقائمة لا يتغير ترتيبها أبدًا ولا تُدرج فيها عناصر أو تُحذف من وسطها. إذا كانت العناصر تتحرك، يربط مفتاح الفهرس الحالة بالموضع، فينتهي النص المكتوب ومربعات الاختيار والتركيز على العنصر الخطأ.
ماذا أستخدم كمفتاح؟
معرّفًا ينتمي إلى البيانات: معرّف قاعدة البيانات، أو رمز المنتج، أو اسم المستخدم. وللعناصر المنشأة في المتصفح، ولّد معرّفًا مرة واحدة عند إنشاء العنصر (عدّادًا أو crypto.randomUUID())، ولا تولّده أبدًا أثناء العرض.
هل يجب أن تكون المفاتيح فريدة في التطبيق كله؟
لا. يكفي أن تكون المفاتيح فريدة بين الأشقاء في القائمة نفسها. يمكن لقائمتين مختلفتين استخدام المفاتيح نفسها.
هل يستطيع المكوّن قراءة مفتاحه؟
لا. تستخدم React الخاصية key ولا تمررها إلى المكوّن كـ prop. إذا احتاج المكوّن إلى المعرّف، فمرّره مرة أخرى باسم آخر، مثل id={item.id}.
كيف أصلح الرسالة "Each child in a list should have a unique key prop"؟
أضف key إلى العنصر الخارجي الذي تُرجعه من map، مستخدمًا معرّفًا ثابتًا من العنصر. إذا كان ذلك العنصر جزءًا، فاستخدم <Fragment key={id}>، لأن <> لا تقبل مفتاحًا.