React: الحالة المعقدة: useReducer
آخر تحديث: 2026-10-26
1. ما ستتعلمه
- النمط الأساسي لـ useReducer (الحالة + الإجراء → الحالة الجديدة)
- مبادئ تصميم أنواع الإجراءات
- المقارنة مع useState (متى تستخدم أيًّا منهما)
- مزيج useReducer + Context (نسخة خفيفة من Redux)
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) |
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).📖 ملخص
useReducer(reducer, initialState)العودة[state, dispatch]- المُخفِّض هو دالة بحتة:
(state, action) => newState - تحتوي «action» على
type(وصف لما حدث) وpayloadاختياري (البيانات) useStateمناسب للحالات البسيطة والمستقلة، بينماuseReducerمناسب للحالات المعقدة والمترابطةuseReducer + Contextيمكن استخدامه كبديل لـ Redux في المشاريع الصغيرة
📝 تمارين
- تمرين أساسي (مستوى الصعوبة ⭐): أنشئ مكونًا باسم
CounterواستخدمuseReducerلتنفيذ أربع عمليات: +1، و-1، و+10، وإعادة الضبط. اعرض القيمة الحالية وسجل العمليات (آخر 5 عمليات) على الصفحة. - تمرين متقدم (مستوى الصعوبة ⭐⭐): أنشئ مكونًا باسم
TodoAppواستخدمuseReducerوContextلتنفيذ خمس وظائف: الإضافة، ووضع علامة «مكتمل»، والتحرير، والحذف، ومسح العناصر المكتملة. - التحدي (الصعوبة: ⭐⭐⭐): أنشئ مكونًا باسم
UndoRedoListواستخدمuseReducerلتنفيذ وظيفة «التراجع/الإعادة». يجب أن تحفظ كل عملية لقطة من السجل وأن تدعم مفتاحي Ctrl+Z للتراجع وCtrl+Shift+Z للإعادة.