EduFlow
أساسيات React
⁦0%⁩

الوحدة 2 · الـ Components والـ Props

عرض القوائم باستخدام الـ keys

22 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

اعرض القوائم من المصفوفات باستخدام map، وصفِّها ورتّبها من دون تعديل البيانات، واختر keys تحافظ على هوية كل عنصر وعلى الـ state الخاص به بين مرّات الـ render.

  1. 1من البيانات إلى العناصر باستخدام map
  2. 2ما وظيفة الـ keys
  3. 3اختيار الـ key
  4. 4الـ keys على الـ components والـ fragments

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

Watchlist قائمة. وكذلك صفحة المنشورات، وجدول الطلبات، ونتائج البحث، والقائمة الرئيسية. أغلب الواجهات قوائم من أشياء مبنية من مصفوفات بيانات، ونهج React هو ما قد تخمّنه من JavaScript نفسها: حوّل مصفوفة البيانات إلى مصفوفة عناصر.

من البيانات إلى العناصر باستخدام map

src/components/MovieList.jsx
export default function MovieList({ movies }) {
  return (
    <ul>
      {movies.map((movie) => (
        <li key={movie.id}>
          {movie.title} ({movie.year})
        </li>
      ))}
    </ul>
  );
}

تُرجع map مصفوفة من عناصر <li>، وتعرض React المصفوفات بعرض كل عنصر فيها بالترتيب. لا توجد صياغة حلقات خاصة لتتعلّمها؛ إنها دالة المصفوفات التي تعرفها أصلًا.

والتصفية والترتيب الفكرة نفسها: جهّز المصفوفة، ثم طبّق عليها map. افعل ذلك أثناء الـ render، من البيانات التي لديك. القائمة الظاهرة مشتقّة، فلا تحتاج إلى أن تكون state.

JavaScript
const movies = [
  { id: 'a1', title: 'Arrival', year: 2016, watched: true },
  { id: 'p2', title: 'Past Lives', year: 2023, watched: false },
  { id: 'd3', title: 'Dune: Part Two', year: 2024, watched: false },
];

const toWatch = movies
  .filter((m) => !m.watched)
  .toSorted((a, b) => b.year - a.year);

console.log(toWatch.map((m) => m.title)); // ["Dune: Part Two", "Past Lives"]
console.log(movies[0].title);             // Arrival (the original order is untouched)

تُرجع toSorted نسخة مرتّبة، وهي مدعومة في كل المتصفّحات الحالية. أمّا sort الأقدم فتعيد ترتيب المصفوفة في مكانها. وإذا استُدعيت على state أو props، فهي تعدّل بصمت بيانات لا تملكها. وإن احتجت إلى دعم متصفّحات أقدم، فانسخ أولًا: [...movies].sort(…).

ما وظيفة الـ keys

كل عنصر في قائمة يحتاج إلى prop اسمه key، وتحذّرك React في الـ console حين يغيب. ويتّضح السبب حين تتغيّر القائمة.

حين تعيد React رسم قائمة، تكون لديها مصفوفة العناصر القديمة والجديدة. وعليها أن تطابق بينهما: أي عنصر جديد هو نفسه أي عنصر قديم؟ هذا مهمّ لأن كل عنصر قد يملك state، مثل ملاحظة نصف مكتوبة في حقل، أو زر عليه التركيز، أو قائمة مفتوحة، ويجب أن يبقى هذا الـ state مع العنصر الصحيح.

الـ key هو بطاقة اسم العنصر. تطابق React العناصر القديمة والجديدة حسب الـ key. الـ key نفسه: إنه العنصر نفسه، فتحتفظ React بالـ DOM والـ state الخاصين به وتحدّث ما تغيّر. key جديد: عنصر جديد، فتُنشئه React. key غائب: أُزيل العنصر، فتهدمه React.

قبل: لدى Arrival ملاحظة. بعد إضافة Dune في الأعلى مع keys الـ index، صار الـ key صفر يشير إلى Dune، فتقفز الملاحظة إلى Dune. ومع keys المعرّف، تبقى الملاحظة مع Arrival. قبل 0 · Arrival · "Loved it" 1 · Past Lives key={index} 0 · Dune · "Loved it" 1 · Arrival 2 · Past Lives key={movie.id} d3 · Dune a1 · Arrival · "Loved it" p2 · Past Lives الـ key نفسه يعني الصف نفسه: الـ state يتبع الـ key keys الـ index تتبع الموضع، وkeys المعرّف تتبع الفيلم
keys الـ index مقابل keys المعرّف بعد الإدراج في الأعلى

اختيار الـ key

الـ key الجيد فريد بين الإخوة وثابت: للعنصر نفسه الـ key نفسه في كل render.

  • استخدم معرّفًا من بياناتك. صفوف قاعدة البيانات وكائنات الـ API لها معرّف أصلًا.
  • أنشئ المعرّفات حين تُنشأ العناصر. للأفلام المضافة عبر نموذج، ولّد المعرّف في المعالج الذي يضيفها: { id: crypto.randomUUID(), title, year }. يُخزَّن على الكائن ولا يتغيّر أبدًا.
  • لا تولّد الـ keys أثناء الـ render أبدًا. key={Math.random()} أو key={crypto.randomUUID()} داخل map ينتج keys جديدة في كل render. فتعامل React كل صف على أنه جديد تمامًا، وتهدم الـ DOM، وتُضيّع تركيز الحقل والـ state مع كل حرف تكتبه.
  • لا تستخدم محتوى غير فريد. العناوين تتكرّر؛ هناك عدّة أفلام اسمها “Dune”.

لا يلزم أن تكون الـ keys فريدة إلا بين الإخوة في القائمة نفسها. ويمكن لقائمتين مختلفتين استخدام المعرّفات نفسها.

الـ keys على الـ components والـ fragments

يوضع الـ key على العنصر الخارجي الذي تُرجعه دالة الـ callback في map. وحين يكون ذلك component، ضعه على الـ component:

JSX
{movies.map((movie) => (
  <MovieItem key={movie.id} movie={movie} />
))}

لا يستطيع MovieItem قراءة key؛ فـ React تستخدمه ولا تمرّره إلى الأسفل. وإذا احتاج الـ component إلى المعرّف، فمرّره بشكل منفصل كـ prop.

وحين يرسم كل عنصر عدّة إخوة من دون غلاف، استخدم الصيغة الطويلة للـ fragment، فهي تقبل key:

JSX
import { Fragment } from 'react';

{movies.map((movie) => (
  <Fragment key={movie.id}>
    <dt>{movie.title}</dt>
    <dd>{movie.year}</dd>
  </Fragment>
))}

الصيغة القصيرة <>…</> لا تقبل key.

صار بإمكانك عرض أي قائمة بشكل صحيح. وبهذا يكتمل Watchlist الثابت. في القسم التالي يبدأ بالحركة: الـ state يتيح للقائمة أن تتغيّر حين ينقر المستخدم ويكتب ويرسل.

أهم النقاط

  • حوّل مصفوفة البيانات إلى مصفوفة عناصر باستخدام map، وضع الـ key على العنصر الخارجي الذي تُرجعه دالة الـ callback.
  • يُخبر الـ key React بهوية كل عنصر بين مرّات الـ render، لذا يجب أن يكون فريدًا بين الإخوة وثابتًا مع الوقت.
  • استخدم معرّفًا (id) من بياناتك كـ key؛ وأنشئ المعرّفات لحظة إنشاء العناصر، لا أثناء الـ render أبدًا.
  • الـ keys المبنية على الـ index تنكسر حين يمكن إعادة ترتيب القائمة أو تصفيتها أو الإدراج فيها، لأن الـ state يلتصق بالموضع بدل العنصر.
  • صفِّ نُسخًا من بياناتك ورتّبها أثناء الـ render؛ فالدالة sort تغيّر المصفوفة في مكانها، لذا استخدم toSorted أو رتّب نسخة.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

أين يوضع الـ key؟

JSX
function MovieList({ movies }) {
  return (
    <ul>
      {movies.map((movie) => (
        <MovieItem movie={movie} />
      ))}
    </ul>
  );
}
السؤال 2 من 4

لكل صف فيلم حقل نصي لملاحظة. كتبت «Loved it» بجانب Arrival، ثم أضفت فيلمًا جديدًا في أعلى القائمة. مع key={index}، أين تنتهي «Loved it»؟

السؤال 3 من 4

أيّ key اختيار جيد للأفلام التي يضيفها المستخدم من خلال نموذج؟

السؤال 4 من 4

تأتي movies من الـ state. ما المشكلة في const sorted = movies.sort((a, b) => b.year - a.year); داخل كود الـ render؟

تدريب

بيئة تجريب الكود

keys تُبقي الملاحظات في مكانها

لكل صف فيلم حقل لملاحظة. هناك مشكلتان:

  1. يجب أن تعرض القائمة الأفلام من الأحدث إلى الأقدم (حسب year). رتّب نسخة أثناء الـ render، ولا تغيّر الـ state المسمّى movies.
  2. اكتب ملاحظة بجانب Arrival، ثم انقر Add Dune: Part Two. تقفز الملاحظة إلى الفيلم الخطأ. أصلح الـ key حتى تبقى كل ملاحظة مع فيلمها.

أبقِ الـ markup كما هو: <li> واحد لكل فيلم، فيه <span> (العنوان والسنة) و<input>.

import { useState } from 'react';

const INITIAL = [
  { id: 'a1', title: 'Arrival', year: 2016 },
  { id: 'p2', title: 'Past Lives', year: 2023 },
];

export default function App() {
  const [movies, setMovies] = useState(INITIAL);

  function addDune() {
    setMovies([{ id: 'd3', title: 'Dune: Part Two', year: 2024 }, ...movies]);
  }

  // 1. Show the movies newest first, without changing `movies` itself.
  // 2. Fix the key so each note stays with its movie.
  return (
    <main>
      <button onClick={addDune}>Add Dune: Part Two</button>
      <ul>
        {movies.map((movie, index) => (
          <li key={index}>
            <span>{movie.title} ({movie.year})</span>
            <input aria-label="Note" placeholder="Note" />
          </li>
        ))}
      </ul>
    </main>
  );
}
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • تظهر الأفلام من الأحدث إلى الأقدم: Past Lives ثم Arrival
  • بعد إضافة Dune، ما زالت ملاحظة Arrival بجانب Arrival
  • يظهر Dune: Part Two (2024) أولًا بعد إضافته