لماذا useReducer؟
حين تصبح الحالة معقّدة (عدة قيم مترابطة، أو تحديثات متعدّدة المنطق)، يصبح useState فوضويًا. useReducer ينظّم منطق التحديث في مكان واحد.
الفكرة: reducer + dispatch
- reducer: دالة تأخذ الحالة الحالية و"إجراءً (action)" وتُرجع الحالة الجديدة.
- dispatch: نطلق بها الإجراءات.
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>تصفير</button>
</>
);
}
مثال أقوى: قائمة مهام
function todosReducer(todos, action) {
switch (action.type) {
case "add":
return [...todos, { id: Date.now(), text: action.text, done: false }];
case "toggle":
return todos.map((t) =>
t.id === action.id ? { ...t, done: !t.done } : t
);
case "delete":
return todos.filter((t) => t.id !== action.id);
default:
return todos;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<button onClick={() => dispatch({ type: "add", text: "مهمة" })}>
أضف
</button>
);
}
كل منطق التحديث في مكان واحد منظّم.
useReducer مقابل useState
| useState | useReducer | |
|---|---|---|
| الأنسب لـ | حالة بسيطة | حالة معقّدة/مترابطة |
| المنطق | متفرّق | مركّز في reducer |
🔑 ابدأ بـ
useState. حين يكثر منطق التحديث أو تترابط القيم، انتقل إلىuseReducer. ويتكامل ممتازًا معuseContextلإدارة حالة عامّة.
نمط متقدم: Reducer مع Domain Exceptions
عند التعامل مع أخطاء API داخل reducer، استخدم ApiError و NetworkError من react-base-repository بدلاً من Error العادي:
import { useReducer } from "react";
import { ApiError, NetworkError, UserDTO, HttpClient, HttpUserRepository } from "./lib/react-base-repository";
interface UserState {
users: UserDTO[];
loading: boolean;
error: string | null;
}
type UserAction =
| { type: "FETCH_START" }
| { type: "FETCH_SUCCESS"; payload: UserDTO[] }
| { type: "FETCH_ERROR"; payload: string };
function userReducer(state: UserState, action: UserAction): UserState {
switch (action.type) {
case "FETCH_START":
return { ...state, loading: true, error: null };
case "FETCH_SUCCESS":
return { users: action.payload, loading: false, error: null };
case "FETCH_ERROR":
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
// استخدام reducer مع Repository
const http = new HttpClient("/api");
const userRepo = new HttpUserRepository(http);
function UserList() {
const [state, dispatch] = useReducer(userReducer, { users: [], loading: false, error: null });
async function loadUsers() {
dispatch({ type: "FETCH_START" });
try {
const users = await userRepo.getUsers();
dispatch({ type: "FETCH_SUCCESS", payload: users });
} catch (err) {
if (err instanceof ApiError) {
dispatch({ type: "FETCH_ERROR", payload: err.message });
} else if (err instanceof NetworkError) {
dispatch({ type: "FETCH_ERROR", payload: "تعذّر الاتصال بالخادم" });
} else {
dispatch({ type: "FETCH_ERROR", payload: "حدث خطأ غير متوقع" });
}
}
}
if (state.error) return <p role="alert">{state.error}</p>;
return (
<div>
<button onClick={loadUsers} disabled={state.loading}>تحميل</button>
{state.loading ? <p>جارٍ التحميل...</p> : (
<ul>{state.users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
)}
</div>
);
}
الأخطاء الشائعة
- ❌ تعديل الحالة مباشرةً داخل reducer → أرجِع كائنًا/مصفوفة جديدة دائمًا.
- ❌ نسيان
defaultفي الـ reducer → أرجِع الحالة كما هي. - ❌ استخدام useReducer لحالة بسيطة →
useStateأبسط.
خلاصة
useReducer ينظّم الحالة المعقّدة: عرّف reducer يعالج الإجراءات، واطلقها بـ dispatch({ type }). أرجِع حالة جديدة دائمًا (لا تعدّل الأصل). مثالي للحالة المترابطة، خاصةً مع Context.