الخطوات
-
1
اعرف متى يكون Context الأداة المناسبة
Context مناسب عندما:
- تحتاج الـ state أن تُقرأ من مكوّنات على مستويات تعشق مختلفة (الثيم، اللغة، المستخدم الحالي، رمز المصادقة، السلة)
- تمرير props أصبح مؤلماً — تمرر نفس الـ prop عبر 3+ طبقات لا تستخدمه بنفسها
Context غير مناسب عندما:
- تتغير الـ state كثيراً (كل ضغطة مفتاح، حدث تمرير، إطار animation) — راجع التحذير في الخطوة الأخيرة
- الـ state محلية لشجرة فرعية واحدة — مرر props أو ارفع الـ state
-
2
أنشئ سياقين منفصلين
أكثر خطأ شائع في Context هو وضع قيمة الـ state ودالة dispatch في سياق واحد. أي مكوّن يستهلكه سيُعيد الرسم عند تغيّر الـ state، حتى لو كان يحتاج فقط لإرسال actions. افصلهما من البداية.
jsximport { createContext } from 'react'; // المكوّنات المستهلكة لهذا السياق تُعيد الرسم عند تغيّر الـ state export const CartStateContext = createContext(null); // المكوّنات المستهلكة لهذا السياق لا تُعيد الرسم أبداً بسبب تغيّر الـ state // (dispatch ثابت — React يضمن أنه لا يتغيير أبداً) export const CartDispatchContext = createContext(null); -
3
تعريف الـ reducer
الـ reducer هو دالة نقية:
(state, action) => newState. يجب ألا تُعدّل الـ state مباشرةً أبداً — دائماً أعد كائناً جديداً. احتفظ به في ملفه الخاص لسهولة الاختبار.javascript// cart-reducer.js export const initialState = { items: [], total: 0 }; export function cartReducer(state, action) { switch (action.type) { case 'ADD_ITEM': { const exists = state.items.find(i => i.id === action.item.id); const items = exists ? state.items.map(i => i.id === action.item.id ? { ...i, qty: i.qty + 1 } : i ) : [...state.items, { ...action.item, qty: 1 }]; return { items, total: items.reduce((s, i) => s + i.price * i.qty, 0) }; } case 'REMOVE_ITEM': return { ...state, items: state.items.filter(i => i.id !== action.id), total: state.total - (state.items.find(i => i.id === action.id)?.price ?? 0), }; case 'CLEAR': return initialState; default: return state; } } -
4
بناء مكوّن Provider
الـ Provider يلفّ تطبيقك (أو الشجرة الفرعية التي تحتاج الـ state) ويمتلك استدعاء
useReducer. يوفر الـ state وdispatch عبر سياقيهما المنفصلين. هذا هو المكان الوحيد الذي يُستدعى فيهuseReducer.jsximport { useReducer } from 'react'; import { CartStateContext, CartDispatchContext } from './cart-context'; import { cartReducer, initialState } from './cart-reducer'; export function CartProvider({ children }) { const [state, dispatch] = useReducer(cartReducer, initialState); return ( <CartDispatchContext.Provider value={dispatch}> <CartStateContext.Provider value={state}> {children} </CartStateContext.Provider> </CartDispatchContext.Provider> ); } -
5
لفّ التطبيق بالـ Provider
ركّب الـ Provider في أعلى نقطة في الشجرة التي تحتاج الوصول لهذه الـ state. عادةً هذا قرب جذر التطبيق، لكن ليس بالضرورة — إذا كان تدفق الدفع فقط يحتاج السلة، لفّ تلك الشجرة الفرعية فقط.
jsx// main.jsx أو App.jsx import { CartProvider } from './cart-provider'; function App() { return ( <CartProvider> <Header /> <main> <ProductList /> <CheckoutFlow /> </main> </CartProvider> ); } -
6
استهلاك الـ state وdispatch في المكوّنات الفرعية
المكوّنات التي تقرأ الـ state تستهلك
CartStateContext. المكوّنات التي تُرسل actions فقط تستهلكCartDispatchContext. زر "أضف إلى السلة" يحتاج dispatch فقط — لن يُعيد الرسم أبداً بسبب تغيّر الـ state.jsximport { useContext } from 'react'; import { CartStateContext, CartDispatchContext } from './cart-context'; // للقراءة فقط: يُعيد الرسم عند تغيّر state السلة function CartSummary() { const { items, total } = useContext(CartStateContext); return ( <div> <span>{items.length} منتج</span> <span>${total.toFixed(2)}</span> </div> ); } // للكتابة فقط: لا يُعيد الرسم أبداً بسبب تغيّر state السلة function AddToCartButton({ product }) { const dispatch = useContext(CartDispatchContext); return ( <button onClick={() => dispatch({ type: 'ADD_ITEM', item: product })}> أضف إلى السلة </button> ); } -
7
أنشئ custom hooks لواجهة أنظف
تصدير السياقات الخام يُجبر كل مستهلك على استيراد ملفين واستدعاء
useContextمباشرةً. لفّهما في hooks مسماة. هذا يتيح لك أيضاً إضافة حارس يطرح خطأً مفيداً إذا استُخدم الـ hook خارج Provider خاصه.javascriptimport { useContext } from 'react'; import { CartStateContext, CartDispatchContext } from './cart-context'; export function useCartState() { const ctx = useContext(CartStateContext); if (ctx === null) throw new Error('useCartState يجب استخدامه داخل <CartProvider>'); return ctx; } export function useCartDispatch() { const ctx = useContext(CartDispatchContext); if (ctx === null) throw new Error('useCartDispatch يجب استخدامه داخل <CartProvider>'); return ctx; } // الاستخدام في أي مكان في الشجرة: // const { items } = useCartState(); // const dispatch = useCartDispatch(); -
8
اعرف متى يكون Context الخيار الخاطئ
Context يُعيد رسم كل مكوّن مستهلك عند تغيّر قيمته. للـ state التي تتغير كثيراً في الثانية — حقل نموذج عند كل ضغطة مفتاح، animation، موضع الماوس — يسبب هذا إعادات رسم متتالية لا يمكن إصلاحها بـ
useMemoأوReact.memoوحدهما. للـ state عالية التردد، استخدم مكتبة متخصصة:- Zustand — بسيطة، بدون كود اعتيادي، قائمة على الاشتراك (فقط المكوّنات المشتركة تُعيد الرسم)
- Jotai — state ذرية، اشتراكات دقيقة
- Redux Toolkit — يستحق في المشاريع الكبيرة مع أنماط فريق صارمة
Context هو الخيار الصحيح للـ state التي تتغير نادراً: المصادقة، الثيم، اللغة، feature flags.
نصائح ومحاذير
- نمط السياق المنقسم (سياق القيمة مقابل سياق dispatch) هو أكثر تحسين واحد مؤثر في Context. طبّقه في كل مرة.
- <code>React.memo</code> يلفّ مكوّناً ويمنع إعادة رسمه إذا لم تتغير props خاصته — ادمجه مع نمط السياق المنقسم لأقصى تأثير.
- سياقات صغيرة متعددة أفضل من سياق كبير واحد. <code>ThemeContext</code> و<code>AuthContext</code> و<code>CartContext</code> أفضل من <code>AppContext</code> واحد يحمل كل شيء.
- يمكنك تعشيق Providers بحرية. <code>CartProvider</code> داخل <code>AuthProvider</code> داخل <code>ThemeProvider</code> هو نمط صالح تماماً.
- لا تضع دوالاً في قيم context إلا إذا كانت ثابتة (ملفوفة بـ <code>useCallback</code>). مرجع دالة جديد عند كل رسم يجعل قيمة الـ context تبدو "متغيّرة" ويُطلق إعادات الرسم.
خاتمة
Context مع useReducer هو حل خفيف الوزن ومدمج لإدارة الـ state العامة لا يتطلب تثبيت أي شيء. الانضباط الرئيسي هو تقسيم الـ state وdispatch في سياقات منفصلة، وتعريف reducer نظيف، ومعرفة الحدود — الـ state التي تتغير نادراً مناسبة تماماً؛ الـ state التي تتغير باستمرار ليست كذلك. لكل ما هو أبعد من هذه الحدود، Zustand أو Redux Toolkit هما الخطوة التالية الصحيحة.