useReducer خطاف في React يحتفظ بالحالة مثل useState، لكنه ينقل كل طرق تغييرها إلى دالة واحدة، هي المختزل (reducer). يستدعي مكوّنك dispatch بكائن إجراء مثل { type: 'increment' }، فتمرر React الحالة الحالية وذلك الإجراء إلى المختزل، الذي يُرجع الحالة التالية.
لا تقول الأزرار كيف يتغير العدد، بل ما حدث فقط. كل الحساب يعيش في reducer. أضف case 'double' يُرجع { count: state.count * 2 } وزرًا يرسله لترى كيف يندمج نوع جديد من التحديثات.
الصيغة
const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional
function reducer(state, action) {
// return the next state
}
reducerدالة(state, action) => nextState. عرّفها خارج المكوّن؛ فهي لا تحتاج إلى أي شيء من نطاق المكوّن.initialArgهي الحالة الابتدائية، وتُستخدم في العرض الأول فقط.initاختيارية. إذا مررتها، تكون الحالة الابتدائيةinit(initialArg)بدلًا من ذلك.stateهي الحالة الحالية لهذا العرض.- يرسل
dispatch(action)إجراءً إلى المختزل ويجدول عرضًا بالنتيجة. وهو ثابت: الدالة نفسها في كل عرض، فمن الآمن تمريره إلى الأسفل أو إدراجه كاعتمادية.
يمكن للإجراء أن يكون أي قيمة، لكنه اصطلاحًا كائن فيه نص type يصف ما حدث، إضافة إلى أي بيانات يحتاجها المختزل: { type: 'added', text: 'Buy milk' }.
كتابة مختزل
معظم المختزلات عبارة switch على action.type، مع case واحدة لكل نوع من الأحداث. كل حالة تُرجع حالة جديدة تمامًا؛ ولا تغيّر القديمة أبدًا. والحالة default ترمي خطأ، فيفشل خطأ إملائي مثل dispatch({ type: 'incremnet' }) بصوت عالٍ بدلًا من ألا يفعل شيئًا.
سمِّ الإجراءات بحسب ما فعله المستخدم (added وtoggled وdeleted)، لا بحسب تغيير الحالة الذي في ذهنك (setTodos). عندها يُقرأ المكوّن كقائمة أحداث، ويكون المختزل المكان الوحيد الذي يقرر معنى كل حدث.
يجب أن تكون المختزلات نقية
المختزل دالة نقية: بالحالة والإجراء نفسيهما يُرجع النتيجة نفسها، ولا يفعل شيئًا آخر. وهذا يعني لا تعديل مباشر، ولا طلبات، ولا مؤقتات، ولا Math.random() أو Date.now() بداخله. تعتمد React على ذلك. في التطوير مع StrictMode، تستدعي React مختزلك مرتين لكل إجراء وتحتفظ بنتيجة واحدة، لتساعدك على ملاحظة المختزل غير النقي. أما المعاينة هنا فتعمل مثل بناء الإنتاج، فتستدعيه الأمثلة في هذه الصفحة مرة واحدة.
قاعدة عدم التعديل المباشر هي الأكثر خرقًا. الإضافة إلى state.todos ثم إرجاع state يُرجع الكائن نفسه، فلا ترى React أي تغيير وتتخطى العرض، وهو الفخ نفسه الموصوف في تحديث المصفوفات والكائنات. ابنِ مصفوفات وكائنات جديدة بالنشر وmap وfilter.
// Wrong: mutates and returns the same object
case 'added':
state.todos.push(action.todo);
return state;
// Right: returns a new object with a new array
case 'added':
return { ...state, todos: [...state.todos, action.todo] };
الآثار الجانبية التي تنتمي إلى حدث، مثل الحفظ على خادم، توضع في معالج الحدث بجانب dispatch. والآثار الجانبية التي تنتمي إلى الحالة، مثل مزامنتها مع localStorage، توضع في تأثير.
قائمة مهام باستخدام useReducer
إليك المختزل يؤدي عملًا حقيقيًا: إضافة المهام وتبديلها وحذفها. يطبع console.log في أعلى المختزل كل إجراء، وهي طريقة مفيدة لمراقبة تغيّر الحالة أثناء تصحيح الأخطاء. إنه الأثر الجانبي الوحيد المقبول عادة في المختزل، لأنه لا يغيّر شيئًا؛ أزله قبل النشر.
أضف مهمة، وحدّد مربعًا واحذف مهمة، ثم اقرأ وحدة التحكم: كل تغيير سطر واحد يسمّي الإجراء وبياناته. يبقى نص الحقل في useState، لأنه محلي للحقل ولا يلمسه أي حدث آخر. المزج بين الخطافين في مكوّن واحد أمر طبيعي.
يُنشأ معرّف المهمة الجديدة في معالج النقر وينتقل في الإجراء، فلا يفعل المختزل إلا نسخه. كتابة id: nextId++ داخل المختزل ستجعله غير نقي: مع StrictMode في التطوير سيتخطى الاستدعاء الثاني معرّفًا.
لا يغيّر dispatch الحالة فورًا
يعمل dispatch مثل دالة الضبط في useState: يجدول عرضًا، ويحتفظ المتغير state في معالجك الحالي بالقيمة القديمة. لاستخدام الحالة الجديدة فورًا، احسبها بنفسك باستدعاء المختزل.
انقر بضع مرات. سطر السجل الأول متأخر دائمًا بخطوة، والثاني يطابق الرقم على الزر بعد العرض. لأن المختزل دالة عادية، فاستدعاؤه بنفسك آمن، وهذه فائدة أخرى لإبقائه نقيًا.
useState مقابل useReducer
يخزن كلا الخطافين الحالة، وكل ما تكتبه بأحدهما يمكنك كتابته بالآخر. الفرق هو أين يعيش منطق التحديث.
| useState | useReducer | |
|---|---|---|
| منطق التحديث | في كل معالج حدث | في دالة مختزل واحدة |
| مناسب لـ | بضع قيم مستقلة | عدة حقول تغيّرها أحداث كثيرة معًا |
| حجم الكود | أقل للحالة البسيطة | أكثر في البداية، وأقل كلما زادت الأحداث |
| الاختبار | اختبر عبر المكوّن | اختبر المختزل كدالة عادية |
| تصحيح الأخطاء | ابحث عن المعالج الذي ضبطها | سجّل كل إجراء في مكان واحد |
الجأ إلى useReducer عندما تلاحظ أن الحالة نفسها تُحدَّث في معالجات كثيرة، أو عندما يجب أن يغيّر حدث واحد عدة قطع من الحالة يجب أن تبقى متسقة، أو عندما يكون المعالج في معظمه منطقًا عن الحالة التالية. أما لمفتاح تبديل أو حقل نصي، فـ useState أقصر وأوضح. والتحويل لاحقًا ليس صعبًا: استبدل دوال الضبط بعمليات dispatch وانقل المنطق إلى حالات.
التهيئة الكسولة
إذا كتبت الحالة الابتدائية كاستدعاء دالة، مثل useReducer(reducer, createInitialState('Ada'))، فإن ذلك الاستدعاء يعمل في كل عرض، رغم أن React لا تستخدم نتيجته إلا في المرة الأولى. إذا كان بناء الحالة الابتدائية مكلفًا، أو ينبغي اشتقاقه من prop، فمرّر وسيطًا ثالثًا: دالة init. تستدعي React init(initialArg) مرة واحدة، في العرض الأول.
اكتب في الحقل: يُعرض المكوّن مع كل ضغطة مفتاح، لكن createInitialState لا تسجّل إلا مرة واحدة. غيّر الآن الاستدعاء إلى useReducer(reducer, createInitialState('Ada')) واكتب مرة أخرى. تعمل الدالة في كل عرض، وترمي React النتيجة في كل مرة بعد الأولى.
مرّر الدالة نفسها، لا نتيجة استدعائها. useReducer(reducer, 'Ada', createInitialState) كسول؛ أما useReducer(reducer, createInitialState('Ada')) فليس كذلك.
useReducer مع السياق
يقترن المختزل جيدًا بالسياق عندما تحتاج شجرة عميقة إلى الحالة. ضع الحالة وdispatch في السياق في الأعلى، فيستطيع أي مكوّن في الأسفل قراءة الحالة أو إرسال الإجراءات دون props تُمرَّر عبر كل طبقة. ولأن dispatch لا يتغير أبدًا، يمكن للمكوّنات التي ترسل الإجراءات فقط أن تقرأ سياقًا منفصلًا لـ dispatch وتتخطى إعادة العرض عندما تتغير الحالة.
import { createContext, useContext, useReducer } from 'react';
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);
export function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
</TodosContext>
);
}
function AddTodo() {
const dispatch = useContext(TodosDispatchContext);
return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}
في React 19 يعمل كائن السياق كمزوّد بنفسه (<TodosContext value={...}>)؛ وما زال <TodosContext.Provider> يعمل أيضًا. تتناول صفحة useContext كيف يصل السياق إلى المكوّنات ومتى يعيد عرضها.
الأسئلة الشائعة
ما هو useReducer في React؟
خطاف لحالة تُوصف تحديثاتها كإجراءات. تستدعي const [state, dispatch] = useReducer(reducer, initialState)، ثم dispatch({ type: 'added' }) من معالجات الأحداث. تمرر React الحالة الحالية والإجراء إلى reducer الخاص بك، وما يُرجعه يصبح الحالة التالية.
متى أستخدم useReducer بدلًا من useState؟
عندما تحدّث عدة أحداث الحالة نفسها بطرق مترابطة، أو عندما تعتمد الحالة التالية على عدة حقول معًا، أو عندما يطول منطق التحديث فتريده في دالة واحدة قابلة للاختبار خارج المكوّن. أما لقيمة أو قيمتين مستقلتين فـ useState أبسط.
لماذا يجب أن يكون المختزل نقيًا؟
قد تستدعي React المختزل أكثر من مرة للإجراء نفسه (يفعل StrictMode ذلك عمدًا في التطوير) وتتوقع النتيجة نفسها في كل مرة. لذا يجب ألا يعدّل المختزل الحالة مباشرة، ولا يجلب بيانات، ولا يضبط مؤقتات، ولا يقرأ قيمًا عشوائية. افعل هذه الأشياء في معالجات الأحداث أو التأثيرات.
هل يحدّث dispatch الحالة فورًا؟
لا. مثل دالة الضبط في useState، يجدول dispatch عرضًا. داخل معالج الحدث الحالي ما زالت state تحمل القيمة القديمة؛ وتظهر الحالة الجديدة في العرض التالي.
ما هو الوسيط الثالث في useReducer؟
دالة init اختيارية. عندما تمررها، تحسب React الحالة الابتدائية على أنها init(initialArg)، وفي العرض الأول فقط. وهي مفيدة عندما يكون بناء الحالة الابتدائية مكلفًا أو يعتمد على prop.