React: الحالة المعقدة: useReducer

آخر تحديث: 2026-10-26

1. ما ستتعلمه



2. قصة حول إدارة حالة سلة التسوق

(1) المشكلة: التفاعلات المعقدة بين عدة مثيلات لـ useState

تقوم أليس بإنشاء مكون لعربة التسوق يتعين عليه التعامل مع تفاعلات متعددة:

JSX
function ShoppingCart() {
  const [items, setItems] = useState([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)
  const [coupon, setCoupon] = useState(null)

  // Add Item
  function addItem(product) {
    const existing = items.find(i => i.id === product.id)
    if (existing) {
      // ❌ Several setState The call order depends on the current value
      setItems(prev => prev.map(i =>
        i.id === product.id ? { ...i, qty: i.qty + 1 } : i
      ))
    } else {
      setItems(prev => [...prev, { ...product, qty: 1 }])
    }
  }

  // Apply a Coupon
  function applyCoupon(code) {
    setIsLoading(true)
    fetch('/api/coupon/' + code)
      .then(res => res.json())
      .then(data => {
        if (data.valid) {
          setCoupon(data)
          setError(null)
        } else {
          setError('Coupon is invalid')
        }
      })
      .catch(err => setError(err.message))
      .finally(() => setIsLoading(false))
  }

  // Question:items、isLoading、error、coupon The four states are interrelated
  // One operation (e.g. applyCoupon) triggers 3 setState calls
  // It's easy to miss an update to a particular state,Causes inconsistencies
}
▶ جرّب الكود

(2) حل useReducer

JSX
// 1. Define the Initial State (seeded with 1 product so the cart is visible)
const initialState = {
  items: [
    { id: 1, name: 'React 101', qty: 1, price: 59 }
  ],
  isLoading: false,
  error: null,
  coupon: null,
  total: 59
}

// 2. Definition reducer(Pure Functions:Receive Previous State + action → Return to the new state)
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existing = state.items.find(i => i.id === action.product.id)
      const newItems = existing
        ? state.items.map(i =>
            i.id === action.product.id ? { ...i, qty: i.qty + 1 } : i
          )
        : [...state.items, { ...action.product, qty: 1 }]
      return { ...state, items: newItems, error: null }
    }
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.id)
      }
    case 'UPDATE_QTY':
      return {
        ...state,
        items: state.items.map(i =>
          i.id === action.id ? { ...i, qty: Math.max(1, action.qty) } : i
        )
      }
    case 'APPLY_COUPON_REQUEST':
      return { ...state, isLoading: true, error: null }
    case 'APPLY_COUPON_SUCCESS':
      return { ...state, isLoading: false, coupon: action.coupon }
    case 'APPLY_COUPON_FAILURE':
      return { ...state, isLoading: false, error: action.error }
    case 'CLEAR_ERROR':
      return { ...state, error: null }
    case 'RESET_CART':
      return initialState
    default:
      return state
  }
}

// 3. Using in a component
function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, initialState)
  const [code, setCode] = useState('')

  function handleAdd(product) {
    dispatch({ type: 'ADD_ITEM', product })
  }

  // Simulated async request for the demo (swap in a real fetch in a real app)
  function handleApplyCoupon() {
    dispatch({ type: 'APPLY_COUPON_REQUEST' })
    setTimeout(() => {
      if (code.trim().toUpperCase() === 'SAVE10') {
        dispatch({ type: 'APPLY_COUPON_SUCCESS', coupon: { code: code.trim().toUpperCase() } })
      } else {
        dispatch({ type: 'APPLY_COUPON_FAILURE', error: 'Coupon is invalid (try SAVE10)' })
      }
    }, 400)
  }

  return (
    <div>
      <p>Items in cart:{state.items.reduce((n, i) => n + i.qty, 0)}</p>
      {state.items.map(item => (
        <div key={item.id} style={{ marginBottom: 6 }}>
          {item.name} × {item.qty}
          <button onClick={() => dispatch({ type: 'UPDATE_QTY', id: item.id, qty: item.qty + 1 })}>+</button>
          <button onClick={() => dispatch({ type: 'REMOVE_ITEM', id: item.id })}>Delete</button>
        </div>
      ))}
      <button onClick={() => handleAdd({ id: Date.now(), name: 'React in Action', qty: 1, price: 79 })}>
        ➕ Add React in Action
      </button>
      <div style={{ marginTop: 12 }}>
        <input
          value={code}
          onChange={e => setCode(e.target.value)}
          placeholder="Enter coupon code"
          style={{ padding: 6 }}
        />
        <button onClick={handleApplyCoupon}>Apply coupon</button>
      </div>
      {state.isLoading && <p>Processing......</p>}
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state.coupon && <p>Coupon has been applied:{state.coupon.code}</p>}
    </div>
  )
}
▶ جرّب الكود

المزايا: يتم تجميع جميع التغييرات في الحالة في دالة واحدة هي reducer — وهي دالة يمكن التنبؤ بها، وقابلة للاختبار، وسهلة التصحيح. ويُعد كل إجراء بمثابة سجل لـ«ما حدث».



3. النمط الأساسي لـ useReducer

المفهوم الوصف مثال
reducer دالة خالصة، (الحالة، الإجراء) → الحالة_الجديدة function cartReducer(state, action)
action كائن يصف «ما حدث» { type: 'ADD_ITEM', payload: product }
dispatch دالة لإرسال إجراء dispatch({ type: 'INCREMENT' })
initialState كائن الحالة الأولية { items: [], total: 0 }
init الدالة التهيئة المؤجلة (المعلمة الثالثة الاختيارية) useReducer(reducer, initialArg, init)
100%
graph LR
    A[Components] -->|dispatch(action)| B[reducer]
    B -->|(state, action) => newState| C[New Status]
    C -->|Update| A
    D[User Actions] -->|Trigger| A
    
    style B fill:#1890ff,color:#fff
    style C fill:#52c41a,color:#fff

(1) قواعد النحو الأساسية

JSX
const [state, dispatch] = useReducer(reducer, initialState)

// reducer It is a pure function:
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    case 'SET_NAME':
      return { ...state, name: action.payload }
    default:
      return state
  }
}
▶ جرّب الكود

الناتج:

TEXT
مقارنة جنبًا إلى جنب: صياغة HTML وصياغة JSX تعرضان ترميزًا مكافئًا

(2) مبادئ تصميم الإجراءات

المبدأ الوصف مثال سيئ مثال جيد
استخدم صيغة الماضي عند ذكر أسماء الأنواع تصف «ما حدث بالفعل» ADD ITEM_ADDED
نقل الحد الأدنى من البيانات لا تنقل سوى البيانات الضرورية؛ واحسب الباقي في المُختصر dispatch({ type: 'SET_TOTAL', total: 42 }) dispatch({ type: 'ITEM_ADDED', item })، حيث يتم حساب total في المُختصر
لا تتعامل مع الآثار الجانبية يجب أن تكون المُخفِّضات دوالًا نقية (لا تستدعي واجهات برمجة التطبيقات (APIs) ولا تقرأ أو تكتب في localStorage) case 'LOGIN': fetch(...) استدعِ واجهات برمجة التطبيقات (APIs) في useEffect أو معالجات الأحداث، ثم قم بالإرسال بعد الانتهاء


4. useReducer مقابل useState

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

▶ مثال: مقارنة بين طرق تنفيذ مختلفة لنفس الوظيفة

▶ جرّب الكود

الناتج:

TEXT
Count: 0 | Last Action: none | Operations: 0. +1→INCREMENT، -1→DECREMENT، Reset→initial، Set 100. يسجل History كل العمليات.


5. useReducer + Context: حالة عامة خفيفة الوزن

عندما تحتاج مكونات متعددة إلى مشاركة حالة المُختصر نفسه، استخدم Context لتمرير dispatch:

▶ عرض المثال القابل للتشغيل (86 سطرًا)
▶ جرّب الكود

6. مثال كامل: نموذج متعدد الخطوات (المعالج)

▶ عرض المثال القابل للتشغيل (194 سطرًا)
▶ جرّب الكود

الناتج:

TEXT
يعرض: "Reset". الحالة: count (setter: setCount). الأزرار: setCount(c => c + 1)}>+1، setCount(0)}>Reset، dispatch({ type: 'INCREMENT' })}>+1، dispatch({ type: 'DECREMENT' })}>-1. يدير useReducer الحالة المعقدة

الناتج المتوقع: نموذج تسجيل مكون من 3 خطوات (الحساب → الملف الشخصي → التفضيلات)، مع التحقق من صحة البيانات في كل خطوة، وإمكانية التقدم والرجوع بين الخطوات، والإرسال النهائي الذي يتبعه عرض نتيجة الإرسال.


▶ المثال 2: التحميل غير المتزامن للبيانات وuseReducer

▶ عرض المثال القابل للتشغيل (117 سطرًا)
▶ جرّب الكود

الناتج:

TEXT
النقر على User 1/2/3 → ⏳ Loading... → 👤 بطاقة (User 1، user1@example.com، Administrator، 2026-01-15). "Trigger error" → ❌ "Invalid User ID"

الناتج:

TEXT
محرر Undo/Redo: أدخل نصًا → Save. Undo يتراجع، وRedo يستعيد. يعرض مكدس الحالات في الماضي/المستقبل

▶ المثال 3: تنفيذ وظيفة «التراجع/الإعادة» باستخدام useReducer

▶ جرّب الكود

الناتج:

TEXT
الإدخال → Save → "Current: hello". Undo → الرجوع إلى الخطوة السابقة. Redo → الاستعادة. العرض: Past: [v1, v2] | Future: [v4]

الناتج:

TEXT
نموذج تسجيل عبر useReducer: username، email، password، role، مربع اختيار agree. تتحقق الحقول عند الإرسال. Reset يفرغ كل المحتوى

▶ المثال 4: استخدام useReducer لإدارة حالة النموذج

▶ جرّب الكود

الناتج:

TEXT
نموذج تسجيل عبر useReducer (Username، Email، Password، مربع اختيار Agree). إرسال غير صالح → رسالة خطأ حمراء. Reset → تفريغ كل الحقول.

الناتج:

TEXT
سياق عربة التسوق: قائمة منتجات مع زر "Add". تعرض عربة التسوق المنتجات + الإجمالي. "Clear" يفرغ عربة التسوق

▶ المثال 5: استخدام useReducer + Context لإنشاء سلة تسوق عامة

▶ عرض المثال القابل للتشغيل (62 سطرًا)
▶ جرّب الكود

الناتج:

TEXT
المنتجات (Keyboard $79، Mouse $49، Monitor $399) مع زر "Add". عربة التسوق: المنتج × الكمية + الإجمالي. "Clear" يفرغ عربة التسوق. حالة عامة عبر Context.


❓ أسئلة شائعة

س لماذا يجب أن يكون المُخفِّض دالة نقية؟
ج الدالة النقية تعني: نفس المدخلات → نفس المخرجات؛ ولا تُعدِّل المتغيرات الخارجية؛ ولا تترتب عليها أي آثار جانبية (لا تستدعي واجهات برمجة التطبيقات (APIs) ولا تقرأ الملفات أو تكتب فيها). يعتمد React على قابلية التنبؤ بمُخفِّضات الحالة من أجل تحسين الأداء (تخطي عمليات العرض غير الضرورية) وتصحيح الأخطاء (السفر عبر الزمن). قد يؤدي استدعاء واجهات برمجة التطبيقات داخل مُخفِّض الحالة إلى تغييرات غير متوقعة في الحالة، مما يضعف تجربة تصحيح الأخطاء.
س متى يجب التبديل من useState إلى useReducer؟
ج عند حدوث أي من الحالات الثلاث التالية: ① عندما يتطلب إجراء واحد تحديث ثلاث حالات أو أكثر من useState (على سبيل المثال، النقر على زر يؤدي في آن واحد إلى تحديث isLoading وerror وdata)؛ ② تكون منطقية الحالة معقدة للغاية لدرجة أنها موزعة عبر عدة دوال لمعالجة الأحداث، مما يجعل تتبعها صعبًا؛ ③ يحتوي المكون على عدة قيم فرعية للحالة تعتمد على بعضها البعض. متى يجب التبديل: عندما يتسبب تنفيذ useState في تجاوز عدد أسطر الكود في المكون 100 سطر.
س هل يمكنني تضمين آثار جانبية (مثل طلبات الاسترداد) في مُخفِّض؟
ج بالطبع لا. يجب أن يكون المُخفِّض دالة نقية — أي يجب أن يُرجع دائمًا نفس الناتج لنفس المدخلات، ويجب ألا يكون له أي آثار جانبية، مثل استدعاءات واجهة برمجة التطبيقات (API) أو عمليات DOM أو الأرقام العشوائية. يجب وضع الآثار الجانبية في useEffect أو دوال معالجة الأحداث. تؤدي كتابة الآثار الجانبية في المُخفِّض إلى: حالة غير متوقعة، وصعوبة في تصحيح الأخطاء، وتعطل ميزة «السفر عبر الزمن» (إعادة التشغيل في DevTools).

📖 ملخص


📝 تمارين

  1. تمرين أساسي (مستوى الصعوبة ⭐): أنشئ مكونًا باسم Counter واستخدم useReducer لتنفيذ أربع عمليات: +1، و-1، و+10، وإعادة الضبط. اعرض القيمة الحالية وسجل العمليات (آخر 5 عمليات) على الصفحة.
  2. تمرين متقدم (مستوى الصعوبة ⭐⭐): أنشئ مكونًا باسم TodoApp واستخدم useReducer وContext لتنفيذ خمس وظائف: الإضافة، ووضع علامة «مكتمل»، والتحرير، والحذف، ومسح العناصر المكتملة.
  3. التحدي (الصعوبة: ⭐⭐⭐): أنشئ مكونًا باسم UndoRedoList واستخدم useReducer لتنفيذ وظيفة «التراجع/الإعادة». يجب أن تحفظ كل عملية لقطة من السجل وأن تدعم مفتاحي Ctrl+Z للتراجع وCtrl+Shift+Z للإعادة.
Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%