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

آخر تحديث: 2026-08-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
const initialState = {
  items: [],
  isLoading: false,
  error: null,
  coupon: null,
  total: 0
}

// 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)

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

  function handleApplyCoupon(code) {
    dispatch({ type: 'APPLY_COUPON_REQUEST' })
    fetch('/api/coupon/' + code)
      .then(res => res.json())
      .then(data => {
        if (data.valid) {
          dispatch({ type: 'APPLY_COUPON_SUCCESS', coupon: data })
        } else {
          dispatch({ type: 'APPLY_COUPON_FAILURE', error: 'Coupon is invalid' })
        }
      })
      .catch(err => dispatch({ type: 'APPLY_COUPON_FAILURE', error: err.message }))
  }

  return (
    <div>
      {state.items.map(item => (
        <div key={item.id}>
          {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>
      ))}
      {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
  }
}
▶ جرّب الكود

(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) معقد (قيم فرعية متعددة تؤثر بعضها على بعض)
سهولة القراءة جيدة (سهلة الفهم بنظرة سريعة) جيدة (إدارة مركزية)
قابلية الاختبار يجب اختبار المكونات التي تتطلب عرضًا تعتبر «المُخفِّضات» دوالًا بحتة ويمكن اختبارها مباشرةً
النطاق المناسب المكونات الصغيرة، الحالات المستقلة المكونات المتوسطة إلى الكبيرة، النماذج المعقدة، عربات التسوق

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

JSX 📖 للعرض فقط
// ============================================
// Example:Counter——useState vs useReducer
// ============================================

// ---- useState version(Simple Scenarios)----
function CounterWithState() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  )
}

// ---- useReducer version(Complex Scenarios)----
const initialState = { count: 0, lastAction: null, history: [] }

function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
        lastAction: 'INCREMENT',
        history: [...state.history, 'INCREMENT']
      }
    case 'DECREMENT':
      return {
        count: state.count - 1,
        lastAction: 'DECREMENT',
        history: [...state.history, 'DECREMENT']
      }
    case 'RESET':
      return initialState
    case 'SET':
      return { ...state, count: action.value }
    default:
      return state
  }
}

function CounterWithReducer() {
  const [state, dispatch] = useReducer(counterReducer, initialState)
  return (
    <div>
      <p>Count:{state.count}</p>
      <p>Last Operation:{state.lastAction}</p>
      <p>Number of operations:{state.history.length}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'SET', value: 100 })}>Set as 100</button>
    </div>
  )
}
47 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

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

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

JSX
// ============================================
// Example:Todo Applications(useReducer + Context)
// Features:Global State Management,Any child component can dispatch
// ============================================

// ---- 1. Create Context ----
const TodoContext = React.createContext(null)

// ---- 2. Definition reducer ----
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.text, done: false }]
    case 'TOGGLE_TODO':
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      )
    case 'DELETE_TODO':
      return state.filter(t => t.id !== action.id)
    case 'CLEAR_DONE':
      return state.filter(t => !t.done)
    default:
      return state
  }
}

// ---- 3. Provider Package ----
function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, [
    { id: 1, text: 'Study useReducer', done: true },
    { id: 2, text: 'Understanding Context Usage', done: false }
  ])

  return (
    <TodoContext.Provider value={{ todos, dispatch }}>
      {children}
    </TodoContext.Provider>
  )
}

// ---- 4. Reading from Any Subcomponent/Edit Status ----
function AddTodo() {
  const [text, setText] = useState('')
  const { dispatch } = useContext(TodoContext)

  function handleSubmit(e) {
    e.preventDefault()
    if (!text.trim()) return
    dispatch({ type: 'ADD_TODO', text: text.trim() })
    setText('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={e => setText(e.target.value)} placeholder="Add to To-Do List..." />
      <button type="submit">Add</button>
    </form>
  )
}

function TodoList() {
  const { todos, dispatch } = useContext(TodoContext)

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
          <input type="checkbox" checked={todo.done}
            onChange={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })} />
          {todo.text}
          <button onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}>✕</button>
        </li>
      ))}
    </ul>
  )
}

function TodoStats() {
  const { todos } = useContext(TodoContext)
  const done = todos.filter(t => t.done).length
  const pending = todos.length - done

  return (
    <p style={{ fontSize: '13px', color: '#666' }}>
      Total {todos.length} | Completed {done} | Pending completion {pending}
      {done > 0 && (
        <button onClick={() => dispatch({ type: 'CLEAR_DONE' })} style={{ marginLeft: '8px' }}>
          Clear Completed
        </button>
      )}
    </p>
  )
}

// ---- 5. Top-level usage ----
function App() {
  return (
    <TodoProvider>
      <div style={{ maxWidth: '500px', margin: '0 auto' }}>
        <h2>📋 Todo(useReducer + Context)</h2>
        <AddTodo />
        <TodoStats />
        <TodoList />
      </div>
    </TodoProvider>
  )
}


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

JSX
// ============================================
// Complete Example:Multi-Step Registration Form(Wizard)
// Features:useReducer Managing the Complex State of Multi-Step Forms
// ============================================

const wizardInitialState = {
  currentStep: 1,
  formData: {
    account: { username: '', email: '', password: '' },
    profile: { name: '', bio: '', avatar: '' },
    preferences: { language: 'zh', theme: 'light', newsletter: false }
  },
  errors: {},
  isSubmitting: false,
  completed: false
}

function wizardReducer(state, action) {
  switch (action.type) {
    case 'NEXT_STEP':
      if (state.currentStep >= 3) return state
      return { ...state, currentStep: state.currentStep + 1, errors: {} }

    case 'PREV_STEP':
      if (state.currentStep <= 1) return state
      return { ...state, currentStep: state.currentStep - 1 }

    case 'UPDATE_FIELD':
      return {
        ...state,
        formData: {
          ...state.formData,
          [action.section]: {
            ...state.formData[action.section],
            [action.field]: action.value
          }
        }
      }

    case 'SET_ERRORS':
      return { ...state, errors: action.errors }

    case 'SUBMIT_START':
      return { ...state, isSubmitting: true, errors: {} }

    case 'SUBMIT_SUCCESS':
      return { ...state, isSubmitting: false, completed: true }

    case 'SUBMIT_FAILURE':
      return { ...state, isSubmitting: false, errors: { submit: action.error } }

    case 'RESET':
      return wizardInitialState

    default:
      return state
  }
}

function WizardForm() {
  const [state, dispatch] = useReducer(wizardReducer, wizardInitialState)

  function validateStep() {
    const errors = {}
    const data = state.formData[getStepKey(state.currentStep)]
    
    if (state.currentStep === 1) {
      if (!data.username) errors.username = 'Required'
      if (!data.email?.includes('@')) errors.email = 'Invalid email address'
      if (!data.password || data.password.length < 6) errors.password = 'Password must be at least 6 chars'
    }
    if (state.currentStep === 2) {
      if (!data.name) errors.name = 'Required'
    }
    
    return errors
  }

  function handleNext() {
    const errors = validateStep()
    if (Object.keys(errors).length > 0) {
      dispatch({ type: 'SET_ERRORS', errors })
      return
    }
    dispatch({ type: 'NEXT_STEP' })
  }

  function handleSubmit() {
    dispatch({ type: 'SUBMIT_START' })
    // Simulation API Request
    setTimeout(() => {
      dispatch({ type: 'SUBMIT_SUCCESS' })
    }, 1500)
  }

  if (state.completed) {
    return (
      <div style={{ textAlign: 'center', padding: '40px' }}>
        <h2>🎉 Registration Successful!</h2>
        <pre style={{ textAlign: 'left', background: '#f5f5f5', padding: '16px', borderRadius: '4px' }}>
          {JSON.stringify(state.formData, null, 2)}
        </pre>
        <button onClick={() => dispatch({ type: 'RESET' })}>Re-register</button>
      </div>
    )
  }

  const stepKey = getStepKey(state.currentStep)

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>📝 Register(Steps {state.currentStep}/3)</h2>

      {/* Step Indicator */}
      <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
        {['Account Information', 'Personal Information', 'Preferences'].map((label, i) => (
          <div key={i} style={{
            flex: 1, padding: '8px', textAlign: 'center',
            backgroundColor: state.currentStep >= i + 1 ? '#1890ff' : '#f0f0f0',
            color: state.currentStep >= i + 1 ? 'white' : '#999',
            borderRadius: '4px', fontSize: '13px'
          }}>
            {label}
          </div>
        ))}
      </div>

      {/* Form Content */}
      {state.currentStep === 1 && <AccountForm formData={state.formData.account} errors={state.errors} dispatch={dispatch} />}
      {state.currentStep === 2 && <ProfileForm formData={state.formData.profile} errors={state.errors} dispatch={dispatch} />}
      {state.currentStep === 3 && <PreferenceForm formData={state.formData.preferences} errors={state.errors} dispatch={dispatch} />}

      {/* Error Message */}
      {state.errors.submit && <p style={{ color: '#ff4d4f' }}>{state.errors.submit}</p>}

      {/* Control Buttons */}
      <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px' }}>
        <button onClick={() => dispatch({ type: 'PREV_STEP' })} disabled={state.currentStep === 1}
          style={btnStyle(state.currentStep === 1 ? '#d9d9d9' : '#666')}>
          Previous Step
        </button>
        {state.currentStep < 3 ? (
          <button onClick={handleNext} style={btnStyle('#1890ff')}>
            Next Step
          </button>
        ) : (
          <button onClick={handleSubmit} disabled={state.isSubmitting} style={btnStyle(state.isSubmitting ? '#d9d9d9' : '#52c41a')}>
            {state.isSubmitting ? 'Submitting......' : 'Submit Registration'}
          </button>
        )}
      </div>
    </div>
  )
}

function getStepKey(step) {
  return ['account', 'profile', 'preferences'][step - 1]
}

function AccountForm({ formData, errors, dispatch }) {
  return (
    <div>
      <input name="username" value={formData.username} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'account', field: 'username', value: e.target.value })}
        placeholder="Username" style={inputStyle(errors.username)} />
      {errors.username && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.username}</p>}
      <input name="email" value={formData.email} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'account', field: 'email', value: e.target.value })}
        placeholder="Email" style={inputStyle(errors.email)} />
      {errors.email && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.email}</p>}
      <input name="password" type="password" value={formData.password} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'account', field: 'password', value: e.target.value })}
        placeholder="Password" style={inputStyle(errors.password)} />
      {errors.password && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.password}</p>}
    </div>
  )
}

function ProfileForm({ formData, errors, dispatch }) {
  return (
    <div>
      <input name="name" value={formData.name} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'profile', field: 'name', value: e.target.value })}
        placeholder="Name" style={inputStyle(errors.name)} />
      {errors.name && <p style={{ color: '#ff4d4f', fontSize: '12px' }}>{errors.name}</p>}
      <textarea name="bio" value={formData.bio} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'profile', field: 'bio', value: e.target.value })}
        placeholder="Personal Profile" rows={3} style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px', marginBottom: '8px' }} />
    </div>
  )
}

function PreferenceForm({ formData, errors, dispatch }) {
  return (
    <div>
      <p>Interface Language:</p>
      {['zh', 'en', 'ja', 'pt', 'ar'].map(lang => (
        <label key={lang} style={{ marginRight: '12px' }}>
          <input type="radio" checked={formData.language === lang}
            onChange={() => dispatch({ type: 'UPDATE_FIELD', section: 'preferences', field: 'language', value: lang })} />
          {lang.toUpperCase()}
        </label>
      ))}
      <p style={{ marginTop: '16px' }}>Topic:</p>
      <select value={formData.theme} onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'preferences', field: 'theme', value: e.target.value })}
        style={{ width: '100%', padding: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }}>
        <option value="light">Light-colored</option>
        <option value="dark">Dark</option>
      </select>
      <label style={{ display: 'block', marginTop: '12px' }}>
        <input type="checkbox" checked={formData.newsletter}
          onChange={e => dispatch({ type: 'UPDATE_FIELD', section: 'preferences', field: 'newsletter', value: e.target.checked })} />
        Subscribe to the Weekly Tech Report
      </label>
    </div>
  )
}

function inputStyle(hasError) {
  return {
    width: '100%', padding: '8px', marginBottom: '4px',
    border: `1px solid ${hasError ? '#ff4d4f' : '#d9d9d9'}`,
    borderRadius: '4px'
  }
}

function btnStyle(bgColor) {
  return {
    padding: '10px 24px', backgroundColor: bgColor,
    color: 'white', border: 'none', borderRadius: '4px',
    cursor: bgColor === '#d9d9d9' ? 'not-allowed' : 'pointer'
  }
}

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


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

JSX
// ============================================
// Example:Asynchronous Data Loading——useReducer Three States of Management Requests
// Features:Display useReducer How to Handle It Gracefully loading / success / error Three States
// ============================================

import { useReducer, useEffect } from 'react'

// 1. Defining States and action Type
const initialState = {
  data: null,
  loading: true,
  error: null
}

function dataReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null }
    case 'FETCH_SUCCESS':
      return { data: action.payload, loading: false, error: null }
    case 'FETCH_ERROR':
      return { data: null, loading: false, error: action.error }
    case 'RESET':
      return initialState
    default:
      return state
  }
}

// 2. Used in components
function UserDataFetcher({ userId }) {
  const [state, dispatch] = useReducer(dataReducer, initialState)

  useEffect(() => {
    if (!userId) return

    dispatch({ type: 'FETCH_START' })

    // Simulation API Request
    const timer = setTimeout(() => {
      if (userId <= 0) {
        dispatch({ type: 'FETCH_ERROR', error: 'Invalid User ID' })
      } else {
        // Simulate a successful response
        const mockUser = {
          id: userId,
          name: `User ${userId}`,
          email: `user${userId}@example.com`,
          role: userId === 1 ? 'Administrator' : 'Regular User',
          joinDate: '2026-01-15'
        }
        dispatch({ type: 'FETCH_SUCCESS', payload: mockUser })
      }
    }, 1000)

    return () => {
      clearTimeout(timer)
      dispatch({ type: 'RESET' })
    }
  }, [userId])

  // 3. Rendering Varies Based on State UI
  if (state.loading) {
    return (
      <div style={{ textAlign: 'center', padding: '20px' }}>
        <div style={{ fontSize: '32px', marginBottom: '8px' }}>⏳</div>
        <p style={{ color: '#666' }}>Loading user data...</p>
      </div>
    )
  }

  if (state.error) {
    return (
      <div style={{ textAlign: 'center', padding: '20px', color: '#ff4d4f' }}>
        <div style={{ fontSize: '32px', marginBottom: '8px' }}>❌</div>
        <p>Failed to load:{state.error}</p>
      </div>
    )
  }

  if (!state.data) {
    return <p style={{ color: '#999', textAlign: 'center' }}>Please select a user</p>
  }

  const { name, email, role, joinDate } = state.data
  return (
    <div style={{
      border: '1px solid #e8e8e8',
      borderRadius: '8px',
      padding: '20px',
      maxWidth: '400px',
      margin: '0 auto'
    }}>
      <h3>👤 User Information</h3>
      <table style={{ width: '100%', borderCollapse: 'collapse' }}>
        <tbody>
          {[
            ['Name', name],
            ['Email', email],
            ['Characters', role],
            ['Date of Registration', joinDate]
          ].map(([label, value]) => (
            <tr key={label}>
              <td style={{ padding: '8px', color: '#666', fontWeight: 'bold', width: '80px' }}>{label}</td>
              <td style={{ padding: '8px' }}>{value}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

// Usage
function App() {
  const [userId, setUserId] = useState(1)
  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <h2>📡 Example of Asynchronous Data Loading</h2>
      <div style={{ marginBottom: '12px', display: 'flex', gap: '8px' }}>
        {[1, 2, 3].map(id => (
          <button key={id} onClick={() => setUserId(id)}
            style={{
              padding: '6px 16px',
              backgroundColor: userId === id ? '#1890ff' : '#f0f0f0',
              color: userId === id ? 'white' : '#333',
              border: 'none', borderRadius: '4px', cursor: 'pointer'
            }}>
            User {id}
          </button>
        ))}
        <button onClick={() => setUserId(-1)}
          style={{ padding: '6px 16px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
          Triggered an error
        </button>
      </div>
      <UserDataFetcher userId={userId} />
    </div>
  )
}

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

JSX
function undoReducer(state, action) {
  switch (action.type) {
    case 'SET':
      return { past: [...state.past, state.present], present: action.payload, future: [] }
    case 'UNDO':
      if (state.past.length === 0) return state
      const previous = state.past[state.past.length - 1]
      return { past: state.past.slice(0, -1), present: previous, future: [state.present, ...state.future] }
    case 'REDO':
      if (state.future.length === 0) return state
      const next = state.future[0]
      return { past: [...state.past, state.present], present: next, future: state.future.slice(1) }
    default:
      return state
  }
}

function UndoApp() {
  const [{ past, present, future }, dispatch] = useReducer(undoReducer, {
    past: [], present: '', future: [],
  })
  const [input, setInput] = useState('')

  return (
    <div style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Undo/Redo Editor</h3>
      <input value={input} onChange={e => setInput(e.target.value)} placeholder="Type something..."
        style={{ width: '100%', padding: 8, borderRadius: 4, marginBottom: 8 }} />
      <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
        <button onClick={() => { dispatch({ type: 'SET', payload: input }); setInput('') }}
          style={{ padding: '6px 16px', cursor: 'pointer' }}>Save</button>
        <button onClick={() => dispatch({ type: 'UNDO' })} disabled={past.length === 0}
          style={{ padding: '6px 16px', cursor: past.length ? 'pointer' : 'not-allowed' }}>Undo</button>
        <button onClick={() => dispatch({ type: 'REDO' })} disabled={future.length === 0}
          style={{ padding: '6px 16px', cursor: future.length ? 'pointer' : 'not-allowed' }}>Redo</button>
      </div>
      <p>Current: <strong>{present || '(empty)'}</strong></p>
      <p style={{ fontSize: 12, color: '#999' }}>Past: [{past.join(', ')}] | Future: [{future.join(', ')}]</p>
    </div>
  )
}
▶ جرّب الكود

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

JSX
const initialForm = { username: '', email: '', password: '', role: 'user', agree: false, errors: {} }

function formReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return { ...state, [action.field]: action.value, errors: { ...state.errors, [action.field]: undefined } }
    case 'SET_ERRORS':
      return { ...state, errors: action.errors }
    case 'RESET':
      return initialForm
    default:
      return state
  }
}

function FormWithReducer() {
  const [form, dispatch] = useReducer(formReducer, initialForm)

  function validate() {
    const errors = {}
    if (!form.username.trim()) errors.username = 'Required'
    if (!form.email.includes('@')) errors.email = 'Invalid email'
    if (form.password.length < 6) errors.password = 'At least 6 chars'
    if (!form.agree) errors.agree = 'Must agree'
    return errors
  }

  function handleSubmit(e) {
    e.preventDefault()
    const errors = validate()
    if (Object.keys(errors).length > 0) { dispatch({ type: 'SET_ERRORS', errors }); return }
    alert(`Submitted: ${form.username}`)
  }

  function fieldStyle(key) {
    return { width: '100%', padding: 8, borderRadius: 4, border: form.errors[key] ? '1px solid #ff4d4f' : '1px solid #d9d9d9', marginBottom: 4 }
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Registration</h3>
      <input value={form.username} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'username', value: e.target.value })} placeholder="Username" style={fieldStyle('username')} />
      {form.errors.username && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{form.errors.username}</p>}
      <input value={form.email} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'email', value: e.target.value })} placeholder="Email" style={fieldStyle('email')} />
      {form.errors.email && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{form.errors.email}</p>}
      <input type="password" value={form.password} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'password', value: e.target.value })} placeholder="Password" style={fieldStyle('password')} />
      {form.errors.password && <p style={{ color: '#ff4d4f', fontSize: 12 }}>{form.errors.password}</p>}
      <label style={{ display: 'block', margin: '8px 0' }}>
        <input type="checkbox" checked={form.agree} onChange={e => dispatch({ type: 'UPDATE_FIELD', field: 'agree', value: e.target.checked })} /> I agree
      </label>
      <button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
      <button type="button" onClick={() => dispatch({ type: 'RESET' })} style={{ padding: '8px 16px', marginLeft: 8, cursor: 'pointer' }}>Reset</button>
    </form>
  )
}

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

JSX
const CartContext = createContext(null)

const cartInitial = { items: [], coupon: null }

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD': {
      const existing = state.items.find(i => i.id === action.product.id)
      return {
        ...state,
        items: existing
          ? state.items.map(i => i.id === action.product.id ? { ...i, qty: i.qty + 1 } : i)
          : [...state.items, { ...action.product, qty: 1 }],
      }
    }
    case 'REMOVE':
      return { ...state, items: state.items.filter(i => i.id !== action.id) }
    case 'SET_QTY':
      return { ...state, items: state.items.map(i => i.id === action.id ? { ...i, qty: action.qty } : i) }
    case 'CLEAR':
      return cartInitial
    default:
      return state
  }
}

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, cartInitial)
  const total = useMemo(() => state.items.reduce((sum, i) => sum + i.price * i.qty, 0), [state.items])
  const value = useMemo(() => ({ ...state, total, dispatch }), [state, total])
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>
}

function useCart() { return useContext(CartContext) }

function ProductList() {
  const { dispatch } = useCart()
  const products = [{ id: 1, name: 'Keyboard', price: 79 }, { id: 2, name: 'Mouse', price: 49 }, { id: 3, name: 'Monitor', price: 399 }]
  return (
    <div>
      <h3>Products</h3>
      {products.map(p => (
        <div key={p.id} style={{ display: 'flex', justifyContent: 'space-between', padding: 8, borderBottom: '1px solid #f0f0f0' }}>
          <span>{p.name} - ${p.price}</span>
          <button onClick={() => dispatch({ type: 'ADD', product: p })} style={{ cursor: 'pointer' }}>Add</button>
        </div>
      ))}
    </div>
  )
}

function CartSummary() {
  const { items, total, dispatch } = useCart()
  return (
    <div>
      <h3>Cart ({items.length} items)</h3>
      {items.map(i => (
        <div key={i.id} style={{ display: 'flex', justifyContent: 'space-between', padding: 4 }}>
          <span>{i.name} x{i.qty}</span>
          <span>${i.price * i.qty}</span>
        </div>
      ))}
      <hr />
      <strong>Total: ${total}</strong>
      <button onClick={() => dispatch({ type: 'CLEAR' })} style={{ marginLeft: 8, cursor: 'pointer' }}>Clear</button>
    </div>
  )
}


❓ أسئلة شائعة

س لماذا يجب أن يكون المُخفِّض دالة نقية؟
ج الدالة النقية تعني: نفس المدخلات → نفس المخرجات؛ ولا تُعدِّل المتغيرات الخارجية؛ ولا تترتب عليها أي آثار جانبية (لا تستدعي واجهات برمجة التطبيقات (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%