React: الحالة المعقدة: useReducer
آخر تحديث: 2026-08-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
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) |
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>
)
}
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).📖 ملخص
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 للإعادة.