تحديث المصفوفات (Arrays) والكائنات (Objects) داخل الحالة (State) بشكل صحيح


سنتعلم اليوم كيفية تحديث المصفوفات (Arrays) والكائنات (Objects) داخل حالة (State) مكونات React بشكل صحيح وفعال، مع التركيز على مبدأ الثبات (Immutability) لضمان سلوك متوقع وتحديثات سليمة للواجهة.

سنبني أمثلة عملية توضح أفضل الممارسات لتجنب الأخطاء الشائعة.

الخطوة 1: تحديث كائن بسيط في الحالة

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

ملاحظة تقنية: تعديل الكائن مباشرة (mutation) يمنع React من اكتشاف التغيير لأن المرجع (reference) للكائن يبقى كما هو، مما يؤدي إلى عدم تحديث الواجهة.

لنقم بإنشاء مكون بسيط لتحديث اسم مستخدم:

import React, { useState } from 'react';

function UserProfile() {
  // تعريف حالة الكائن "user" باستخدام useState
  const [user, setUser] = useState({ name: 'أحمد', age: 30, email: 'ahmed@example.com' });

  // دالة لتحديث اسم المستخدم
  const updateUserName = () => {
    // إنشاء نسخة جديدة من الكائن "user" وتعديل خاصية "name"
    setUser(prevUser => ({
      ...prevUser, // نسخ جميع الخصائص الحالية من الكائن السابق
      name: 'فاطمة' // تحديث خاصية الاسم
    }));
  };

  return (
    <div>
      <h3>ملف تعريف المستخدم</h3>
      <p>الاسم: {user.name}</p>
      <p>العمر: {user.age}</p>
      <p>البريد الإلكتروني: {user.email}</p>
      <button onClick={updateUserName}>تحديث الاسم</button>
    </div>
  );
}
// export default UserProfile; // سيتم تجميعها في الكود النهائي

في هذا المثال، نستخدم عامل الانتشار (spread operator) ...prevUser لنسخ جميع الخصائص من الكائن السابق، ثم نقوم بتجاوز (override) خاصية name بالقيمة الجديدة. هذا يضمن أن الكائن الجديد له مرجع مختلف، مما يُعلم React بضرورة إعادة العرض.

الخطوة 2: تحديث مصفوفة من الكائنات في الحالة

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

ملاحظة تقنية: دوال مثل push()، pop()، splice() تعدّل المصفوفة الأصلية. بدلاً منها، استخدم دوال مثل map()، filter()، concat()، أو عامل الانتشار [...] التي تُرجع مصفوفة جديدة.

لنقم بإنشاء مكون لإدارة قائمة المهام:

import React, { useState } from 'react';

function TodoList() {
  // تعريف حالة المصفوفة "todos" باستخدام useState
  const [todos, setTodos] = useState([
    { id: 1, text: 'تعلم React Hooks', completed: false },
    { id: 2, text: 'بناء مشروع صغير', completed: false },
    { id: 3, text: 'مراجعة المفاهيم', completed: true },
  ]);

  // دالة لإضافة مهمة جديدة
  const addTodo = () => {
    const newTodo = {
      id: todos.length + 1, // توليد معرّف فريد بسيط
      text: 'مهمة جديدة',
      completed: false,
    };
    // إنشاء مصفوفة جديدة بإضافة المهمة الجديدة
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };

  // دالة لحذف مهمة
  const deleteTodo = (idToDelete) => {
    // إنشاء مصفوفة جديدة تحتوي على المهام باستثناء المهمة المراد حذفها
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== idToDelete));
  };

  // دالة لتغيير حالة الإنجاز لمهمة معينة
  const toggleTodoCompletion = (idToToggle) => {
    // إنشاء مصفوفة جديدة بتحديث حالة المهمة المطابقة
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === idToToggle // إذا كان هو العنصر المستهدف
          ? { ...todo, completed: !todo.completed } // أنشئ نسخة جديدة من العنصر مع عكس حالة الإنجاز
          : todo // وإلا، أعد العنصر كما هو
      )
    );
  };

  return (
    <div>
      <h3>قائمة المهام</h3>
      <button onClick={addTodo}>إضافة مهمة</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
            <button onClick={() => toggleTodoCompletion(todo.id)}>
              {todo.completed ? 'إلغاء الإنجاز' : 'إنجاز'}
            </button>
            <button onClick={() => deleteTodo(todo.id)}>حذف</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
// export default TodoList; // سيتم تجميعها في الكود النهائي

في هذا الجزء، استخدمنا [...prevTodos, newTodo] لإضافة عنصر، filter() لحذف عنصر، و map() مع عامل الانتشار لتحديث خاصية عنصر معين داخل المصفوفة، كل ذلك مع الحفاظ على مبدأ الثبات.

الخطوة 3: تحديث الكائنات المتداخلة (Nested Objects)

عندما تكون لديك كائنات متداخلة داخل حالتك، تحتاج إلى تطبيق نفس مبدأ الثبات بشكل متكرر لكل مستوى من مستويات التداخل التي تريد تحديثها.

ملاحظة تقنية: لتحديث كائن متداخل، يجب عليك إنشاء نسخة جديدة لكل كائن على المسار من الكائن الجذر إلى الخاصية التي يتم تحديثها.

لنفترض أن لدينا كائن مستخدم يحتوي على كائن عنوان متداخل:

import React, { useState } from 'react';

function NestedObjectUpdate() {
  // تعريف حالة الكائن "userProfile" مع كائن متداخل "address"
  const [userProfile, setUserProfile] = useState({
    id: 1,
    name: 'سارة',
    contact: {
      email: 'sara@example.com',
      phone: '123-456-7890'
    },
    address: {
      street: 'شارع السلام',
      city: 'الرياض',
      zipCode: '12345'
    }
  });

  // دالة لتحديث المدينة في العنوان
  const updateCity = () => {
    setUserProfile(prevProfile => ({
      ...prevProfile, // نسخ خصائص الكائن userProfile
      address: { // تحديث كائن العنوان
        ...prevProfile.address, // نسخ خصائص كائن العنوان الحالي
        city: 'جدة' // تحديث خاصية المدينة
      }
    }));
  };

  // دالة لتحديث البريد الإلكتروني في معلومات الاتصال
  const updateEmail = () => {
    setUserProfile(prevProfile => ({
      ...prevProfile, // نسخ خصائص الكائن userProfile
      contact: { // تحديث كائن الاتصال
        ...prevProfile.contact, // نسخ خصائص كائن الاتصال الحالي
        email: 'sara.new@example.com' // تحديث خاصية البريد الإلكتروني
      }
    }));
  };

  return (
    <div>
      <h3>تحديث الكائنات المتداخلة</h3>
      <p>الاسم: {userProfile.name}</p>
      <p>البريد الإلكتروني: {userProfile.contact.email}</p>
      <p>المدينة: {userProfile.address.city}</p>
      <button onClick={updateCity}>تحديث المدينة إلى جدة</button>
      <button onClick={updateEmail}>تحديث البريد الإلكتروني</button>
    </div>
  );
}
// export default NestedObjectUpdate; // سيتم تجميعها في الكود النهائي

نلاحظ هنا أننا نستخدم عامل الانتشار مرتين: مرة للكائن الجذر userProfile، ومرة أخرى للكائن المتداخل address أو contact. هذا يضمن أن جميع الكائنات على المسار المؤدي إلى الخاصية المحدثة يتم استبدالها بنسخ جديدة.

الكود النهائي الكامل

هذا الكود يجمع الأمثلة السابقة في مكون React واحد (عادةً ما يكون App.js) لتوضيح كيفية عملها معًا.

import React, { useState } from 'react';

function App() {
  // ----------------------------------------------------
  // الخطوة 1: تحديث كائن بسيط
  // ----------------------------------------------------
  const [user, setUser] = useState({ name: 'أحمد', age: 30, email: 'ahmed@example.com' });

  const updateUserName = () => {
    setUser(prevUser => ({
      ...prevUser,
      name: 'فاطمة'
    }));
  };

  // ----------------------------------------------------
  // الخطوة 2: تحديث مصفوفة من الكائنات
  // ----------------------------------------------------
  const [todos, setTodos] = useState([
    { id: 1, text: 'تعلم React Hooks', completed: false },
    { id: 2, text: 'بناء مشروع صغير', completed: false },
    { id: 3, text: 'مراجعة المفاهيم', completed: true },
  ]);

  const addTodo = () => {
    const newTodo = {
      id: todos.length > 0 ? Math.max(...todos.map(t => t.id)) + 1 : 1, // توليد معرّف فريد بشكل أفضل
      text: <code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">مهمة جديدة ${todos.length + 1}</code>,
      completed: false,
    };
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };

  const deleteTodo = (idToDelete) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== idToDelete));
  };

  const toggleTodoCompletion = (idToToggle) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === idToToggle
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };

  // ----------------------------------------------------
  // الخطوة 3: تحديث الكائنات المتداخلة
  // ----------------------------------------------------
  const [userProfile, setUserProfile] = useState({
    id: 1,
    name: 'سارة',
    contact: {
      email: 'sara@example.com',
      phone: '123-456-7890'
    },
    address: {
      street: 'شارع السلام',
      city: 'الرياض',
      zipCode: '12345'
    }
  });

  const updateCity = () => {
    setUserProfile(prevProfile => ({
      ...prevProfile,
      address: {
        ...prevProfile.address,
        city: 'جدة'
      }
    }));
  };

  const updateEmail = () => {
    setUserProfile(prevProfile => ({
      ...prevProfile,
      contact: {
        ...prevProfile.contact,
        email: 'sara.new@example.com'
      }
    }));
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
      <h1>درس: تحديث المصفوفات والكائنات في حالة React</h1>

      <hr />

      {/* عرض الخطوة 1 */}
      <div>
        <h2>الخطوة 1: تحديث كائن بسيط</h2>
        <h3>ملف تعريف المستخدم</h3>
        <p>الاسم: {user.name}</p>
        <p>العمر: {user.age}</p>
        <p>البريد الإلكتروني: {user.email}</p>
        <button onClick={updateUserName}>تحديث الاسم إلى فاطمة</button>
      </div>

      <hr />

      {/* عرض الخطوة 2 */}
      <div>
        <h2>الخطوة 2: تحديث مصفوفة من الكائنات</h2>
        <h3>قائمة المهام</h3>
        <button onClick={addTodo}>إضافة مهمة</button>
        <ul>
          {todos.map(todo => (
            <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none', margin: '5px 0' }}>
              {todo.text}
              <button onClick={() => toggleTodoCompletion(todo.id)} style={{ marginLeft: '10px' }}>
                {todo.completed ? 'إلغاء الإنجاز' : 'إنجاز'}
              </button>
              <button onClick={() => deleteTodo(todo.id)} style={{ marginLeft: '5px' }}>حذف</button>
            </li>
          ))}
        </ul>
      </div>

      <hr />

      {/* عرض الخطوة 3 */}
      <div>
        <h2>الخطوة 3: تحديث الكائنات المتداخلة</h2>
        <h3>ملف تعريف المستخدم (متداخل)</h3>
        <p>الاسم: {userProfile.name}</p>
        <p>البريد الإلكتروني: {userProfile.contact.email}</p>
        <p>الهاتف: {userProfile.contact.phone}</p>
        <p>الشارع: {userProfile.address.street}</p>
        <p>المدينة: {userProfile.address.city}</p>
        <p>الرمز البريدي: {userProfile.address.zipCode}</p>
        <button onClick={updateCity}>تحديث المدينة إلى جدة</button>
        <button onClick={updateEmail} style={{ marginLeft: '10px' }}>تحديث البريد الإلكتروني</button>
      </div>
    </div>
  );
}

export default App;

النتيجة المتوقعة

عند تشغيل هذا الكود في تطبيق React، ستظهر لك صفحة تحتوي على ثلاثة أقسام رئيسية:

  1. ملف تعريف المستخدم: يعرض اسم المستخدم وعمره وبريده الإلكتروني. عند النقر على زر "تحديث الاسم"، سيتغير الاسم من "أحمد" إلى "فاطمة".
  2. قائمة المهام: تعرض قائمة بالمهام. يمكنك إضافة مهام جديدة، حذف المهام الموجودة، وتغيير حالة إنجاز كل مهمة (مع ظهور خط على النص عند الإنجاز).
  3. ملف تعريف المستخدم (متداخل): يعرض تفاصيل المستخدم بما في ذلك معلومات الاتصال والعنوان المتداخلة. عند النقر على "تحديث المدينة إلى جدة"، ستتغير المدينة. وعند النقر على "تحديث البريد الإلكتروني"، سيتغير البريد الإلكتروني.

جميع التحديثات ستتم بشكل فوري على الواجهة، مما يدل على أن حالة المكون يتم تحديثها بشكل صحيح وبمراعاة مبدأ الثبات.