تدفق البيانات بـ useReducer
كل قواعد تغيير الحالة في دالة واحدة. ⏱ نحو 30 دقيقة.
- تشرح الحالة والفعل والمخفّض والإرسال.
- تبني حالة مركبة بـ
useReducer. - تكتب مخفّضًا نقيًّا وتختبره وحده.
- تختار بين
useStateوuseReducer.
١حين تكثر دوال التغيير
في سلة مشتريات حقيقية: إضافة، وحذف، وزيادة الكمية، وإنقاصها، وتفريغ. لو كتبنا كل عملية دالة مبعثرة في المكوّن، صار منطق الحالة موزعًا يصعب اختباره. useReducer يجمع كل قواعد التغيير في دالة واحدة نقية اسمها المخفّض Reducer:
function reducer(state, action) { // تستقبل الحالة الحالية و«فعلًا» يصف ما حدث // وتعيد الحالة الجديدة (نسخة جديدة، لا تعديل)} const [state, dispatch] = useReducer(reducer, initialState);dispatch({ type: 'add', name: 'قهوة' }); // «أرسل» فعلًا
| المصطلح | معناه | مثال |
|---|---|---|
| الحالة state | البيانات الحالية | { items: [], total: 0 } |
| الفعل action | كائن يصف ما حدث، وفيه type على الأقل | { type: 'add', name: 'قهوة', price: 35 } |
| المخفّض reducer | دالة: (حالة، فعل) ← حالة جديدة | function reducer(state, action) |
| الإرسال dispatch | تسلّم الفعل للمخفّض | dispatch({ type: 'clear' }) |
٢عدّاد بـ useReducer
import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'inc': return { count: state.count + 1 }; case 'dec': return { count: Math.max(0, state.count - 1) }; case 'add': return { count: state.count + action.amount }; case 'reset': return { count: 0 }; default: throw new Error('فعل غير معروف: ' + action.type); }} export default function App() { const [state, dispatch] = useReducer(reducer, { count: 0 }); return ( <div> <h2>{state.count}</h2> <button onClick={() => dispatch({ type: 'inc' })}>+1</button>{' '} <button onClick={() => dispatch({ type: 'dec' })}>-1</button>{' '} <button onClick={() => dispatch({ type: 'add', amount: 10 })}>+10</button>{' '} <button onClick={() => dispatch({ type: 'reset' })}>تصفير</button> </div> );}
المكوّن لا يعرف كيف تتغير الحالة؛ هو فقط يصف ما حدث. وقاعدة «لا ينزل العدد تحت الصفر» مكتوبة مرة واحدة في المخفّض.
٣سلة مشتريات كاملة
import { useReducer } from 'react';import { cartReducer, initialCart } from './cartReducer.js'; const menu = [ { id: 1, name: 'قهوة عربية', price: 35 }, { id: 2, name: 'تمر سكري', price: 45 },]; export default function App() { const [cart, dispatch] = useReducer(cartReducer, initialCart); const total = cart.items.reduce((s, i) => s + i.price * i.qty, 0); return ( <div> {menu.map(p => ( <button key={p.id} onClick={() => dispatch({ type: 'add', product: p })}> أضف {p.name} </button> ))} <ul> {cart.items.map(i => ( <li key={i.id}> {i.name} × {i.qty}{' '} <button onClick={() => dispatch({ type: 'dec', id: i.id })}>−</button>{' '} <button onClick={() => dispatch({ type: 'remove', id: i.id })}>حذف</button> </li> ))} </ul> <p>الإجمالي: {total} ريال</p> </div> );}
export const initialCart = { items: [] }; export function cartReducer(state, action) { switch (action.type) { case 'add': { const found = state.items.find(i => i.id === action.product.id); if (found) { return { items: state.items.map(i => i.id === found.id ? { ...i, qty: i.qty + 1 } : i) }; } return { items: [...state.items, { ...action.product, qty: 1 }] }; } case 'dec': return { items: state.items .map(i => i.id === action.id ? { ...i, qty: i.qty - 1 } : i) .filter(i => i.qty > 0), }; case 'remove': return { items: state.items.filter(i => i.id !== action.id) }; default: return state; }}
كل قواعد السلة في ملف cartReducer.js: إضافة منتج موجود تزيد كميته، والإنقاص إلى الصفر يحذفه. ويمكن اختبار هذا الملف وحده بلا واجهة.
٤المخفّض دالة نقية: اختبرها بلا واجهة
المخفّض لا يستدعي الخادم، ولا يقرأ الوقت، ولا يغيّر شيئًا خارجه: نفس المدخلات تعطي نفس المخرجات دائمًا. لذلك يسهل اختباره بـ console.log:
import { cartReducer } from './cartReducer.js'; let s = { items: [] };s = cartReducer(s, { type: 'add', product: { id: 1, name: 'قهوة', price: 35 } });s = cartReducer(s, { type: 'add', product: { id: 1, name: 'قهوة', price: 35 } });console.log('بعد إضافتين:', s.items[0].qty);s = cartReducer(s, { type: 'dec', id: 1 });s = cartReducer(s, { type: 'dec', id: 1 });console.log('بعد إنقاصين:', s.items.length, 'عناصر'); export default function App() { return <p>افتح Console لترى نتيجة الاختبار.</p>;}
export function cartReducer(state, action) { switch (action.type) { case 'add': { const found = state.items.find(i => i.id === action.product.id); if (found) return { items: state.items.map(i => i.id === found.id ? { ...i, qty: i.qty + 1 } : i) }; return { items: [...state.items, { ...action.product, qty: 1 }] }; } case 'dec': return { items: state.items.map(i => i.id === action.id ? { ...i, qty: i.qty - 1 } : i).filter(i => i.qty > 0) }; default: return state; }}
| useState | useReducer | |
|---|---|---|
| المناسب | قيم بسيطة مستقلة | حالة مركّبة بقواعد تغيير كثيرة |
| أين المنطق؟ | داخل معالجات الأحداث | في دالة المخفّض (ملف مستقل) |
| الاختبار | عبر الواجهة | المخفّض وحده بسهولة |
| ما بعده | — | الفكرة نفسها أساس Redux (الدرس القادم) |
توقّع وتحقّق
ما الذي يعيده المخفّض؟
(state, action) ← statedispatch({ type: 'clear' }) تعني:
الدالة النقية:
Pure.متى نفضّل useReducer على useState؟
الخلاصة
const [state, dispatch] = useReducer(reducer, initial).- المخفّض:
(state, action)← حالة جديدة، بلا تعديل ولا آثار جانبية. - الفعل كائن فيه
type، مثل{ type: 'add', product }. switchعلىaction.type، وdefaultللأفعال غير المعروفة.- المنطق في ملف مستقل يسهل اختباره، وهو أساس
Redux.
تحدٍّ برمجي: قائمة مهام بالمخفّض
متوسطقائمة مهام كل منطقها في مخفّض واحد.
- المخفّض يدعم الأفعال:
add(بالنص text)، وtoggle(بالمعرّف id)، وclearDone(يحذف المنجزة). - حقل
<input>وزر «أضف»، وكل مهمة<li>تُعلَّم منجزة بالضغط عليها، وتأخذ الصنفdoneحين تُنجز. - زر «احذف المنجزة» يرسل clearDone.
- استخدم useReducer، ولا تستخدم useState للمهام (يجوز لنص الحقل).
👀 اعرض الناتج المطلوب (وجرّبه)
- الكود يعمل بلا أخطاء في Console
- إضافة ثلاث مهام
- الضغط على مهمة يعلّمها done
- «احذف المنجزة» يحذفها فقط
- تستخدم useReducer و dispatch
- المخفّض فيه الأفعال الثلاثة
💡 تلميح 1
[...state, جديد] و map و filter.💡 تلميح 2
className={t.done ? 'done' : ''}حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
import { useReducer, useState } from 'react'; function reducer(state, action) { switch (action.type) { case 'add': return [...state, { id: state.length ? state[state.length - 1].id + 1 : 1, text: action.text, done: false }]; case 'toggle': return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t); case 'clearDone': return state.filter(t => !t.done); default: return state; }} export default function App() { const [todos, dispatch] = useReducer(reducer, []); const [text, setText] = useState(''); return ( <div> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => { if (text.trim()) dispatch({ type: 'add', text }); setText(''); }}>أضف</button> <ul> {todos.map(t => ( <li key={t.id} className={t.done ? 'done' : ''} onClick={() => dispatch({ type: 'toggle', id: t.id })}>{t.text}</li> ))} </ul> <button onClick={() => dispatch({ type: 'clearDone' })}>احذف المنجزة</button> </div> );}