Menu
flag Ar iconالعربيةdown icon

تحديث المصفوفات والكائنات في حالة React دون تعديلها

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

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

في React تحدّث مصفوفة أو كائنًا في الحالة بصنع واحد جديد وتمريره إلى دالة الضبط. لا تغيّر القيمة الموجودة في مكانها أبدًا: استخدم النشر (...) أو map أو filter أو slice لبناء نسخة فيها تغييرك، ثم استدعِ setItems(copy).

[...items, { ... }] مصفوفة جديدة فيها عنصر إضافي، وfilter تُرجع مصفوفة جديدة دون العنصر المحذوف. وكلاهما يترك المصفوفة القديمة على حالها.

لماذا لا يؤدي التعديل المباشر إلى العرض

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

انقر "push" بضع مرات. تُظهر وحدة التحكم المصفوفة تكبر، لكن لا يظهر سطر عرض جديد ويبقى النص a. ثم انقر "spread": يحدث عرض واحد وتظهر كل العناصر المضافة دفعة واحدة، لأنها كانت في المصفوفة طوال الوقت. التعديل المباشر لا يرمي خطأ، بل يترك الشاشة قديمة حتى يطلق شيء آخر عرضًا. انقر "push" مرة أخرى بعد ذلك وقد يُعرض هذه المرة: لا تستطيع React دائمًا تخطي العمل مسبقًا، فيظهر التعديل أحيانًا ولا يظهر أحيانًا أخرى. وعدم القدرة على التنبؤ هذا هو سبب عدم تعديل الحالة مباشرة.

المصفوفات: توابع تستخدمها وأخرى تتجنبها

القاعدة بسيطة: استخدم التوابع التي تُرجع مصفوفة جديدة، وتجنّب التوابع التي تغيّر المصفوفة التي تُستدعى عليها.

الهدفتجنّب (يعدّل)استخدم (يُرجع مصفوفة جديدة)
الإضافةpush وunshift[...arr, item] و[item, ...arr]
الحذفpop وshift وsplicefilter وslice
الاستبدالarr[i] = x وsplicemap وwith(i, x)
الإدراجsplice[...arr.slice(0, i), x, ...arr.slice(i)] وtoSpliced
الترتيب والعكسsort وreversetoSorted وtoReversed، أو انسخ أولًا

toSorted وtoReversed وtoSpliced وwith توابع مصفوفات أحدث (ES2023)، مدعومة في كل المتصفحات الرئيسية الحالية منذ 2023. تؤدي المهمة نفسها التي تؤديها نظيراتها المعدِّلة، لكن على نسخة.

تحديث عنصر واحد بـ map

لتغيير عنصر واحد، مرّ على المصفوفة بـ map وأرجع كائنًا جديدًا للعنصر الذي تريده، والكائن الأصلي لكل عنصر آخر.

الجزء { ...todo, done: !todo.done } مهم. من الأخطاء الشائعة نسخ المصفوفة مع تغيير الكائن الذي بداخلها:

const next = [...todos];
next[0].done = true; // next is new, but next[0] is the same object as todos[0]
setTodos(next);

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

الإدراج عند فهرس

لا يوجد insert غير معدِّل، لذا اقطع المصفوفة نصفين وضع العنصر الجديد بينهما. ويؤدي toSpliced(index, 0, item) الشيء نفسه في استدعاء واحد.

اضبط الفهرس على 0 وأدرج مرة أخرى: يذهب C الجديد إلى البداية. استبدل التعبير next بـ letters.toSpliced(index, 0, 'C') فتكون النتيجة نفسها.

الترتيب والعكس

يغيّر sort وreverse المصفوفة التي يُستدعيان عليها، رغم أنهما يُرجعانها أيضًا. وهذا يجعل setItems(items.sort()) تعديلًا مباشرًا يتبعه ضبط بالمرجع نفسه، وهو الخطأ الذي بدأت به هذه الصفحة. رتّب نسخة بدلًا من ذلك.

يعمل [...scores].reverse() لأن النشر يصنع النسخة أولًا ثم لا يغيّر reverse إلا تلك النسخة.

تحديث الكائنات والكائنات المتداخلة

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

كل مستوى تنسخه كائن جديد؛ وكل مستوى لا تلمسه يبقى الكائن نفسه. وهذا ما يجعل النشر رخيصًا: لو احتوت الحالة أيضًا على قائمة طويلة من الطلبات، لنسخ { ...user } المرجع إلى القائمة لا القائمة نفسها.

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

structuredClone مقابل النشر

يصنع structuredClone(user) نسخة عميقة في استدعاء واحد، فيمكنك بعدها تغيير النسخة بحرية:

const next = structuredClone(user);
next.address.city = 'Oslo';
setUser(next);

هذا صحيح، لكنه ينسخ الكائن كله في كل مرة، بما في ذلك الأجزاء التي لم تغيّرها. هذا يكلّف وقتًا مع الحالة الكبيرة، ويعني أن كل كائن متداخل يحصل على مرجع جديد، فيُعاد عرض ابن محفوظ يستقبل user.orders رغم أن الطلبات لم تتغير. ويرمي structuredClone أيضًا خطأ مع الدوال، وتعود نسخ الأصناف ككائنات عادية. أما النشر فلا ينسخ إلا المسار الذي تغيّره، ولهذا هو الخيار المعتاد؛ واحتفظ بـ structuredClone للتحديث النادر الذي يعيد كتابة معظم الكائن فعلًا.

كتابة التعديلات باستخدام Immer

Immer مكتبة صغيرة تتيح لك كتابة كود يبدو كتعديل مباشر. تسلّمك مسودة، وتسجّل ما تغيّره، وتنتج قيمة جديدة غير قابلة للتعديل لا يُنسخ فيها إلا الأجزاء المتغيرة. وتغلّفها الحزمة use-immer في خطاف:

import { useImmer } from 'use-immer';

function Profile() {
    const [user, updateUser] = useImmer({
        name: 'Ada',
        address: { city: 'London' },
    });

    function moveTo(city) {
        updateUser((draft) => {
            draft.address.city = city;
        });
    }
    // ...
}

Immer غير متاحة في محررات هذه الصفحة، التي لا تحمّل إلا React. وتستحق الإضافة عندما تكون حالتك متداخلة بعمق ويصعب قراءة النشر. أما للحالة المسطحة فيكفي النشر العادي وmap، وهما ما ستراه في معظم كود React.

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

كيف تضيف عنصرًا إلى مصفوفة في حالة React؟

أنشئ مصفوفة جديدة تحتوي العناصر القديمة والعنصر الجديد ومررها إلى دالة الضبط: setItems([...items, newItem]). لا تستدعِ items.push(newItem)، لأن ذلك يغيّر المصفوفة الموجودة فترى React المرجع نفسه.

لماذا لا يُعاد عرض مكوّني عندما أحدّث مصفوفة؟

على الأرجح غيّرت المصفوفة في مكانها (push أو splice أو sort أو arr[i] = x) ومررت المصفوفة نفسها من جديد. تقارن React القيمة القديمة بالجديدة عبر Object.is، فتجد الكائن نفسه، وتتخطى العرض. مرّر مصفوفة جديدة بدلًا من ذلك.

كيف تحدّث كائنًا واحدًا داخل مصفوفة في React؟

استخدم map وأرجع كائنًا جديدًا للعنصر الذي يتغير، والعنصر الأصلي لكل عنصر آخر: setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t)).

كيف تحدّث كائنًا متداخلًا في حالة React؟

انسخ كل مستوى في الطريق إلى الحقل الذي تغيّره: setUser({ ...user, address: { ...user.address, city: 'Oslo' } }). النشر الواحد لا ينسخ إلا المستوى الأعلى، فيبقى الكائن الداخلي مشتركًا.

هل أستخدم structuredClone أم معامل النشر لحالة React؟

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

رسم توضيحي للغات البرمجة في Coddy

تعلّم البرمجة مع Coddy

ابدأ الآن