البرمجة متوسط 12 دقيقة

كيفية إدارة الحالة العامة في React باستخدام Context و useReducer

Context يحل مشكلة محددة واحدة: إيصال الـ state إلى المكوّنات المتعشقة بعمق دون تمرير props عبر كل طبقة بينهما. إنه ليس مكتبة لإدارة الـ state — بل هو آلية لحقن التبعيات. عند استخدامه بشكل صحيح مع useReducer، يتعامل مع state مشتركة عبر الشجرة مثل المستخدم الحالي، والثيم، أو سلة التسوق. عند استخدامه بشكل خاطئ، يجعل كل مكوّن في شجرتك يُعيد الرسم عند كل تغيير في الـ state. يوضح هذا الدليل كلا الطريقتين الصحيحة والخاطئة.

الخطوات

  1. 1

    اعرف متى يكون Context الأداة المناسبة

    Context مناسب عندما:

    • تحتاج الـ state أن تُقرأ من مكوّنات على مستويات تعشق مختلفة (الثيم، اللغة، المستخدم الحالي، رمز المصادقة، السلة)
    • تمرير props أصبح مؤلماً — تمرر نفس الـ prop عبر 3+ طبقات لا تستخدمه بنفسها

    Context غير مناسب عندما:

    • تتغير الـ state كثيراً (كل ضغطة مفتاح، حدث تمرير، إطار animation) — راجع التحذير في الخطوة الأخيرة
    • الـ state محلية لشجرة فرعية واحدة — مرر props أو ارفع الـ state
  2. 2

    أنشئ سياقين منفصلين

    أكثر خطأ شائع في Context هو وضع قيمة الـ state ودالة dispatch في سياق واحد. أي مكوّن يستهلكه سيُعيد الرسم عند تغيّر الـ state، حتى لو كان يحتاج فقط لإرسال actions. افصلهما من البداية.

    jsx
    import { createContext } from 'react';
    
    // المكوّنات المستهلكة لهذا السياق تُعيد الرسم عند تغيّر الـ state
    export const CartStateContext    = createContext(null);
    
    // المكوّنات المستهلكة لهذا السياق لا تُعيد الرسم أبداً بسبب تغيّر الـ state
    // (dispatch ثابت — React يضمن أنه لا يتغيير أبداً)
    export const CartDispatchContext = createContext(null);
  3. 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. 4

    بناء مكوّن Provider

    الـ Provider يلفّ تطبيقك (أو الشجرة الفرعية التي تحتاج الـ state) ويمتلك استدعاء useReducer. يوفر الـ state وdispatch عبر سياقيهما المنفصلين. هذا هو المكان الوحيد الذي يُستدعى فيه useReducer.

    jsx
    import { 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. 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. 6

    استهلاك الـ state وdispatch في المكوّنات الفرعية

    المكوّنات التي تقرأ الـ state تستهلك CartStateContext. المكوّنات التي تُرسل actions فقط تستهلك CartDispatchContext. زر "أضف إلى السلة" يحتاج dispatch فقط — لن يُعيد الرسم أبداً بسبب تغيّر الـ state.

    jsx
    import { 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. 7

    أنشئ custom hooks لواجهة أنظف

    تصدير السياقات الخام يُجبر كل مستهلك على استيراد ملفين واستدعاء useContext مباشرةً. لفّهما في hooks مسماة. هذا يتيح لك أيضاً إضافة حارس يطرح خطأً مفيداً إذا استُخدم الـ hook خارج Provider خاصه.

    javascript
    import { 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. 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 هما الخطوة التالية الصحيحة.

#React #State #Context
العودة إلى جميع الأدلة

هل تحتاج مساعدة في مشروعك؟

احجز استشارة مجانية لمدة 30 دقيقة لمناقشة تحدياتك التقنية واستكشاف الحلول معًا.