العربية

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

إطلاق العنان للأداء: نظرة معمقة على مفاتيح المواءمة في React لتحسين أداء القوائم

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

من المحتمل أنك كتبت كودًا مثل data.map(item => <div>{item.name}</div>) مرات لا تحصى. يبدو الأمر بسيطًا، يكاد يكون تافهًا. ولكن تحت هذه البساطة يكمن اعتبار أداء حاسم، إذا تم تجاهله، يمكن أن يؤدي إلى تطبيقات بطيئة وأخطاء محيرة. الحل؟ خاصية (prop) صغيرة ولكنها قوية: key.

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

الفصل الأول: فهم عملية المواءمة في React والـ DOM الافتراضي

قبل أن نتمكن من تقدير أهمية المفاتيح، يجب أن نفهم أولاً الآلية الأساسية التي تجعل React سريعة: المواءمة، المدعومة بالـ DOM الافتراضي (VDOM).

ما هو الـ DOM الافتراضي؟

يُعد التفاعل المباشر مع نموذج كائن المستند (DOM) الخاص بالمتصفح مكلفًا حسابيًا. في كل مرة تغير فيها شيئًا في الـ DOM—مثل إضافة عقدة، أو تحديث نص، أو تغيير نمط—يجب على المتصفح القيام بكمية كبيرة من العمل. قد يحتاج إلى إعادة حساب الأنماط والتخطيط للصفحة بأكملها، وهي عملية تُعرف بـ reflow و repaint. في تطبيق معقد يعتمد على البيانات، يمكن أن تؤدي التلاعبات المباشرة والمتكررة في الـ DOM إلى تدهور الأداء بسرعة.

تقدم React طبقة تجريدية لحل هذه المشكلة: الـ DOM الافتراضي. الـ VDOM هو تمثيل خفيف الوزن في الذاكرة للـ DOM الحقيقي. فكر فيه كمخطط لواجهة المستخدم الخاصة بك. عندما تخبر React بتحديث واجهة المستخدم (على سبيل المثال، عن طريق تغيير حالة مكون)، فإن React لا تلمس الـ DOM الحقيقي على الفور. بدلاً من ذلك، تقوم بالخطوات التالية:

  1. يتم إنشاء شجرة VDOM جديدة تمثل الحالة المحدثة.
  2. تتم مقارنة شجرة VDOM الجديدة هذه مع شجرة VDOM السابقة. تسمى عملية المقارنة هذه "diffing".
  3. تكتشف React المجموعة الدنيا من التغييرات المطلوبة لتحويل الـ VDOM القديم إلى الجديد.
  4. يتم بعد ذلك تجميع هذه التغييرات الدنيا وتطبيقها على الـ DOM الحقيقي في عملية واحدة فعالة.

هذه العملية، المعروفة باسم المواءمة، هي ما يجعل React عالية الأداء. فبدلاً من إعادة بناء المنزل بأكمله، تعمل React كمقاول خبير يحدد بدقة الطوب الذي يحتاج إلى استبدال، مما يقلل من العمل والتعطيل.

الفصل الثاني: مشكلة تصيير القوائم بدون مفاتيح

الآن، دعونا نرى أين يمكن أن يواجه هذا النظام الأنيق مشكلة. لننظر في مكون بسيط يقوم بتصيير قائمة من المستخدمين:


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

عندما يتم تصيير هذا المكوّن لأول مرة، تبني React شجرة VDOM. إذا أضفنا مستخدمًا جديدًا إلى *نهاية* مصفوفة `users`، فإن خوارزمية المقارنة في React تتعامل معها برشاقة. تقارن القائمتين القديمة والجديدة، وترى عنصرًا جديدًا في النهاية، وببساطة تضيف `<li>` جديدًا إلى الـ DOM الحقيقي. فعال وبسيط.

ولكن ماذا يحدث إذا أضفنا مستخدمًا جديدًا إلى بداية القائمة، أو أعدنا ترتيب العناصر؟

لنفترض أن قائمتنا الأولية هي:

وبعد التحديث، تصبح:

بدون أي معرفات فريدة، تقارن React القائمتين بناءً على ترتيبهما (الفهرس). إليك ما تراه:

هذا غير فعال بشكل لا يصدق. فبدلاً من مجرد إدراج عنصر جديد واحد لـ "Charlie" في البداية، قامت React بإجراء تعديلين وإدراج واحد. بالنسبة لقائمة كبيرة، أو لعناصر قائمة هي مكونات معقدة لها حالتها الخاصة، يؤدي هذا العمل غير الضروري إلى تدهور كبير في الأداء، والأهم من ذلك، أخطاء محتملة في حالة المكون.

لهذا السبب، إذا قمت بتشغيل الكود أعلاه، ستعرض لك وحدة تحكم المطور في متصفحك تحذيرًا: "Warning: Each child in a list should have a unique 'key' prop." تخبرك React صراحةً أنها بحاجة إلى مساعدة لأداء وظيفتها بكفاءة.

الفصل الثالث: خاصية `key` تأتي للإنقاذ

خاصية `key` هي التلميح الذي تحتاجه React. إنها سمة سلسلة خاصة تقدمها عند إنشاء قوائم من العناصر. تمنح المفاتيح كل عنصر هوية مستقرة وفريدة عبر عمليات إعادة التصيير.

دعنا نعيد كتابة مكون `UserList` الخاص بنا مع المفاتيح:


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

هنا، نفترض أن كل كائن `user` له خاصية `id` فريدة (على سبيل المثال، من قاعدة بيانات). الآن، دعنا نعود إلى سيناريونا.

البيانات الأولية:


[{ id: 'u1', name: 'Alice' }, { id: 'u2', name: 'Bob' }]

البيانات المحدثة:


[{ id: 'u3', name: 'Charlie' }, { id: 'u1', name: 'Alice' }, { id: 'u2', name: 'Bob' }]

مع المفاتيح، تصبح عملية المقارنة في React أكثر ذكاءً:

  1. تنظر React إلى العناصر الأبناء لـ `<ul>` في الـ VDOM الجديد وتتحقق من مفاتيحها. ترى `u3` و `u1` و `u2`.
  2. ثم تتحقق من العناصر الأبناء في الـ VDOM السابق ومفاتيحها. ترى `u1` و `u2`.
  3. تعرف React أن المكونات ذات المفاتيح `u1` و `u2` موجودة بالفعل. لا تحتاج إلى تعديلها؛ تحتاج فقط إلى نقل عقد الـ DOM المقابلة لها إلى مواضعها الجديدة.
  4. ترى React أن المفتاح `u3` جديد. تقوم بإنشاء مكون وعقدة DOM جديدة لـ "Charlie" وتدرجها في البداية.

النتيجة هي عملية إدراج واحدة في الـ DOM وبعض إعادة الترتيب، وهو ما يعتبر أكثر كفاءة بكثير من التعديلات المتعددة والإدراج التي رأيناها من قبل. توفر المفاتيح هوية مستقرة، مما يسمح لـ React بتتبع العناصر عبر عمليات التصيير، بغض النظر عن موقعها في المصفوفة.

الفصل الرابع: اختيار المفتاح الصحيح - القواعد الذهبية

تعتمد فعالية خاصية `key` بشكل كامل على اختيار القيمة الصحيحة. هناك أفضل الممارسات الواضحة والأنماط المضادة الخطيرة التي يجب أن تكون على دراية بها.

أفضل مفتاح: مُعرّفات فريدة ومستقرة

المفتاح المثالي هو قيمة تحدد عنصرًا بشكل فريد ودائم داخل قائمة. هذا دائمًا تقريبًا هو مُعرّف فريد من مصدر بياناتك.

مصادر ممتازة للمفاتيح تشمل:


// جيد: استخدام مُعرّف مستقر وفريد من البيانات.
<div>
  {products.map(product => (
    <ProductItem key={product.sku} product={product} />
  ))}
</div>

النمط السيء: استخدام فهرس المصفوفة كمفتاح

خطأ شائع هو استخدام فهرس المصفوفة كمفتاح:


// سيء: استخدام فهرس المصفوفة كمفتاح.
<div>
  {items.map((item, index) => (
    <ListItem key={index} item={item} />
  ))}
</div>

على الرغم من أن هذا سيسكت تحذير React، إلا أنه يمكن أن يؤدي إلى مشاكل خطيرة ويعتبر بشكل عام نمطًا سيئًا. استخدام الفهرس كمفتاح يخبر React أن هوية العنصر مرتبطة بموقعه في القائمة. هذه هي نفس المشكلة جوهريًا مثل عدم وجود مفتاح على الإطلاق عندما يمكن إعادة ترتيب القائمة أو تصفيتها أو إضافة/إزالة عناصر من البداية أو الوسط.

خطأ إدارة الحالة:

يظهر أخطر الآثار الجانبية لاستخدام مفاتيح الفهرس عندما تدير عناصر القائمة حالتها الخاصة. تخيل قائمة من حقول الإدخال:


function UnstableList() {
  const [items, setItems] = React.useState([{ id: 1, text: 'First' }, { id: 2, text: 'Second' }]);

  const handleAddItemToTop = () => {
    setItems([{ id: 3, text: 'New Top' }, ...items]);
  };

  return (
    <div>
      <button onClick={handleAddItemToTop}>Add to Top</button>
      {items.map((item, index) => (
        <div key={index}>
          <label>{item.text}: </label>
          <input type="text" />
        </div>
      ))}
    </div>
  );
}

جرب هذا التمرين الذهني:

  1. يتم تصيير القائمة مع "First" و "Second".
  2. تكتب "Hello" في حقل الإدخال الأول (الخاص بـ "First").
  3. تنقر على زر "Add to Top".

ماذا تتوقع أن يحدث؟ تتوقع ظهور حقل إدخال جديد فارغ لـ "New Top"، وأن ينتقل حقل الإدخال الخاص بـ "First" (الذي لا يزال يحتوي على "Hello") إلى الأسفل. ما يحدث في الواقع هو أن حقل الإدخال في الموضع الأول (الفهرس 0)، الذي لا يزال يحتوي على "Hello"، يبقى. ولكن الآن أصبح مرتبطًا بعنصر البيانات الجديد، "New Top". حالة مكون الإدخال (قيمته الداخلية) مرتبطة بموقعه (key=0)، وليس بالبيانات التي من المفترض أن يمثلها. هذا خطأ كلاسيكي ومربك تسببه مفاتيح الفهرس.

إذا قمت ببساطة بتغيير `key={index}` إلى `key={item.id}`، يتم حل المشكلة. ستقوم React الآن بربط حالة المكون بشكل صحيح مع المعرف المستقر للبيانات.

متى يكون من المقبول استخدام فهرس كمفتاح؟

هناك مواقف نادرة يكون فيها استخدام الفهرس آمنًا، ولكن يجب أن تستوفي جميع هذه الشروط:

  1. القائمة ثابتة: لن يتم إعادة ترتيبها أو تصفيتها أو إضافة/إزالة عناصر منها إلا من النهاية.
  2. العناصر في القائمة ليس لها معرفات مستقرة.
  3. المكونات التي يتم تصييرها لكل عنصر بسيطة وليس لها حالة داخلية.

حتى في هذه الحالة، غالبًا ما يكون من الأفضل إنشاء مُعرّف مؤقت ولكنه مستقر إذا أمكن. يجب أن يكون استخدام الفهرس دائمًا خيارًا مدروسًا، وليس افتراضيًا.

أسوأ الممارسات: استخدام `Math.random()`

لا تستخدم أبدًا `Math.random()` أو أي قيمة أخرى غير حتمية كمفتاح:


// فظيع: لا تفعل هذا!
<div>
  {items.map(item => (
    <ListItem key={Math.random()} item={item} />
  ))}
</div>

المفتاح الذي يتم إنشاؤه بواسطة `Math.random()` مضمون أن يكون مختلفًا في كل عملية تصيير. هذا يخبر React أن القائمة الكاملة للمكونات من التصيير السابق قد تم تدميرها وتم إنشاء قائمة جديدة تمامًا من مكونات مختلفة تمامًا. هذا يجبر React على إلغاء تحميل جميع المكونات القديمة (وتدمير حالتها) وتحميل جميع المكونات الجديدة. إنه يهزم تمامًا الغرض من المواءمة وهو أسوأ خيار ممكن للأداء.

الفصل الخامس: مفاهيم متقدمة وأسئلة شائعة

المفاتيح و `React.Fragment`

أحيانًا تحتاج إلى إرجاع عناصر متعددة من استدعاء `map`. الطريقة القياسية للقيام بذلك هي باستخدام `React.Fragment`. عند القيام بذلك، يجب وضع `key` على مكون `Fragment` نفسه.


function Glossary({ terms }) {
  return (
    <dl>
      {terms.map(term => (
        // يوضع المفتاح على الـ Fragment، وليس على الأبناء.
        <React.Fragment key={term.id}>
          <dt>{term.name}</dt>
          <dd>{term.definition}</dd>
        </React.Fragment>
      ))}
    </dl>
  );
}

مهم: الصيغة المختصرة `<>...</>` لا تدعم المفاتيح. إذا كانت قائمتك تتطلب أجزاء (fragments)، فيجب عليك استخدام الصيغة الصريحة `<React.Fragment>`.

المفاتيح يجب أن تكون فريدة فقط بين الأشقاء

من المفاهيم الخاطئة الشائعة أن المفاتيح يجب أن تكون فريدة عالميًا عبر تطبيقك بأكمله. هذا غير صحيح. يحتاج المفتاح فقط إلى أن يكون فريدًا داخل قائمته المباشرة من الأشقاء.


function CourseRoster({ courses }) {
  return (
    <div>
      {courses.map(course => (
        <div key={course.id}>  {/* مفتاح للدورة */} 
          <h3>{course.title}</h3>
          <ul>
            {course.students.map(student => (
              // هذا المفتاح للطالب يجب أن يكون فريدًا فقط داخل قائمة طلاب هذه الدورة المحددة.
              <li key={student.id}>{student.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

في المثال أعلاه، يمكن أن يكون لدورتين مختلفتين طالب بالمعرف `id: 's1'`. هذا جيد تمامًا لأن المفاتيح يتم تقييمها داخل عناصر `<ul>` أصل مختلفة.

استخدام المفاتيح لإعادة تعيين حالة المكوّن عمدًا

بينما تستخدم المفاتيح بشكل أساسي لتحسين أداء القوائم، فإنها تخدم غرضًا أعمق: فهي تحدد هوية المكون. إذا تغير مفتاح المكون، فلن تحاول React تحديث المكون الحالي. بدلاً من ذلك، ستقوم بتدمير المكون القديم (وجميع أبنائه) وإنشاء مكون جديد تمامًا من الصفر. هذا يلغي تحميل النسخة القديمة ويحمل نسخة جديدة، مما يعيد تعيين حالته بشكل فعال.

يمكن أن تكون هذه طريقة قوية وتعريفية لإعادة تعيين مكون. على سبيل المثال، تخيل مكون `UserProfile` يجلب البيانات بناءً على `userId`.


function App() {
  const [userId, setUserId] = React.useState('user-1');

  return (
    <div>
      <button onClick={() => setUserId('user-1')}>View User 1</button>
      <button onClick={() => setUserId('user-2')}>View User 2</button>
      
      <UserProfile key={userId} id={userId} />
    </div>
  );
}

من خلال وضع `key={userId}` على مكون `UserProfile`، نضمن أنه كلما تغير `userId`، سيتم التخلص من مكون `UserProfile` بأكمله وإنشاء مكون جديد. هذا يمنع الأخطاء المحتملة حيث قد تبقى حالة من ملف تعريف المستخدم السابق (مثل بيانات النموذج أو المحتوى الذي تم جلبه). إنها طريقة نظيفة وصريحة لإدارة هوية المكون ودورة حياته.

الخاتمة: كتابة كود React أفضل

خاصية `key` هي أكثر بكثير من مجرد طريقة لإسكات تحذير في وحدة التحكم. إنها تعليمات أساسية لـ React، توفر المعلومات الحاسمة اللازمة لعمل خوارزمية المواءمة الخاصة بها بكفاءة وبشكل صحيح. إتقان استخدام المفاتيح هو سمة مميزة لمطور React المحترف.

دعنا نلخص النقاط الرئيسية:

من خلال استيعاب هذه المبادئ، لن تكتب فقط تطبيقات React أسرع وأكثر موثوقية، بل ستكتسب أيضًا فهمًا أعمق لآليات المكتبة الأساسية. في المرة القادمة التي تقوم فيها بالمرور على مصفوفة لتصيير قائمة، امنح خاصية `key` الاهتمام الذي تستحقه. سيشكرك أداء تطبيقك—ونفسك المستقبلية—على ذلك.