useReducer
מבוא
useReducer הוא Hook לניהול State מורכב. הוא מבוסס על אותו רעיון כמו Redux: במקום לעדכן את ה-State ישירות, שולחים "פעולה" (action) לפונקציה אחת מרכזית - ה-reducer - שאחראית על כל לוגיקת העדכון. הוא חלופה ל-useState, ומתאים יותר כאשר:
- ה-State מכיל כמה ערכים הקשורים זה לזה
- לוגיקת העדכון מורכבת ומתפצלת למקרים רבים
- העדכון הבא תלוי במצב הקודם
התחביר הבסיסי
const [state, dispatch] = useReducer(reducer, initialState);
הרכיבים:
- state - המצב הנוכחי
- dispatch - פונקציה לשליחת פעולות (actions)
- reducer - פונקציה שמחשבת את ה-state החדש
- initialState - ערך התחלתי
Reducer Function
ה-reducer הוא לב המנגנון: פונקציה טהורה שמקבלת את ה-State הנוכחי ואת ה-action שנשלח, ומחזירה את ה-State החדש. בדרך כלל בונים אותה כ-switch שמטפל בכל סוג פעולה בנפרד, ותמיד מחזירים אובייקט State חדש במקום לשנות את הקיים:
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'RESET':
return { ...state, count: 0 };
default:
return state;
}
}
דוגמה: Counter
import { useReducer } from 'react';
// הגדרת reducer
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'INCREMENT_BY':
return { count: state.count + action.payload };
case 'RESET':
return { count: 0 };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
// הערך ההתחלתי
const initialState = { count: 0 };
function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<p>ספירה: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
+1
</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>
-1
</button>
<button onClick={() => dispatch({ type: 'INCREMENT_BY', payload: 5 })}>
+5
</button>
<button onClick={() => dispatch({ type: 'RESET' })}>
אפס
</button>
</div>
);
}
דוגמה: Todo List
// סוגי פעולות
const ACTIONS = {
ADD: 'ADD_TODO',
TOGGLE: 'TOGGLE_TODO',
DELETE: 'DELETE_TODO',
EDIT: 'EDIT_TODO',
CLEAR_COMPLETED:'CLEAR_COMPLETED'
};
// Reducer
function todoReducer(state, action) {
switch (action.type) {
case ACTIONS.ADD:
return [
...state,
{
id: Date.now(),
text: action.payload,
completed: false
}
];
case ACTIONS.TOGGLE:
return state.map(todo =>
todo.id === action.payload ?
{ ...todo, completed: !todo.completed }
: todo
);
case ACTIONS.DELETE:
return state.filter(todo => todo.id !== action.payload);
case ACTIONS.EDIT:
return state.map(todo =>
todo.id === action.payload.id ?
{ ...todo, text: action.payload.text }
: todo
);
case ACTIONS.CLEAR_COMPLETED:
return state.filter(todo => !todo.completed);
default:
return state;
}
}
// קומפוננטה
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [text, setText] = useState('');
const handleAdd = () => {
if (text.trim()) {
dispatch({ type: ACTIONS.ADD, payload: text });
setText('');
}
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
/>
<button onClick={handleAdd}>הוסף</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({
type: ACTIONS.TOGGLE,
payload: todo.id
})}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => dispatch({
type: ACTIONS.DELETE,
payload: todo.id
})}>
❌
</button>
</li>
))}
</ul>
<button onClick={() => dispatch({ type: ACTIONS.CLEAR_COMPLETED })}>
נקה הושלמו
</button>
</div>
);
}
דוגמה: Form State
const initialFormState = {
name: '',
email: '',
password: '',
errors: {},
isSubmitting: false,
isSubmitted: false
};
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
[action.field]: action.value,
errors: {
...state.errors,
[action.field]: null // ניקוי שגיאה בעת הקלדה
}
};
case 'SET_ERROR':
return {
...state,
errors: {
...state.errors,
[action.field]: action.error
}
};
case 'SET_ERRORS':
return {
...state,
errors: action.errors
};
case 'SUBMIT_START':
return {
...state,
isSubmitting: true,
errors: {}
};
case 'SUBMIT_SUCCESS':
return {
...state,
isSubmitting: false,
isSubmitted: true
};
case 'SUBMIT_FAILURE':
return {
...state,
isSubmitting: false,
errors: action.errors
};
case 'RESET':
return initialFormState;
default:
return state;
}
}
function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, initialFormState);
const handleChange = (field) => (e) => {
dispatch({ type: 'SET_FIELD', field, value: e.target.value });
};
const validate = () => {
const errors = {};
if (!state.name) errors.name = 'שם נדרש';
if (!state.email.includes('@')) errors.email = 'אימייל לא תקין';
if (state.password.length < 6) errors.password = 'לפחות 6 תווים';
return errors;
};
const handleSubmit = async (e) => {
e.preventDefault();
const errors = validate();
if (Object.keys(errors).length > 0) {
dispatch({ type: 'SET_ERRORS', errors });
return;
}
dispatch({ type: 'SUBMIT_START' });
try {
await submitForm(state);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (error) {
dispatch({
type: 'SUBMIT_FAILURE',
errors: { form: error.message }
});
}
};
if (state.isSubmitted) {
return <p>✅ ההרשמה הצליחה!</p>;
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
value={state.name}
onChange={handleChange('name')}
placeholder="שם"
/>
{state.errors.name && <span className="error">{state.errors.name}</span>}
</div>
<div>
<input
type="email"
value={state.email}
onChange={handleChange('email')}
placeholder="אימייל"
/>
{state.errors.email && <span className="error">{state.errors.email}</span>}
</div>
<div>
<input
type="password"
value={state.password}
onChange={handleChange('password')}
placeholder="סיסמה"
/>
{state.errors.password && <span className="error">{state.errors.password}</span>}
</div>
<button type="submit" disabled={state.isSubmitting}>
{state.isSubmitting ? 'שולח...' : 'הרשמה'}
</button>
</form>
);
}
useState vs useReducer
| תכונה | useState | useReducer |
|---|---|---|
| מורכבות | פשוט | מורכב |
| מספר ערכים | אחד או מעט | הרבה קשורים |
| לוגיקת עדכון | פשוטה | מורכבת |
| תלות בערך קודם | לא מומלץ | מתאים |
| Testing | קשה יותר | קל יותר |
מתי להשתמש ב-useReducer?
// ✅ useState - פשוט
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
// ✅ useReducer - מורכב
const [formState, dispatch] = useReducer(formReducer, initialFormState);
// formState = { name, email, password, errors, isSubmitting, ... }
// ✅ useReducer - עדכונים תלויים
const [history, dispatch] = useReducer(historyReducer, { past: [], present: null, future: [] });
useReducer עם Context
שילוב useReducer עם Context הוא דפוס עוצמתי ונפוץ: ה-reducer מרכז את כל לוגיקת העדכון, וה-Context מפיץ את ה-State ואת פונקציית ה-dispatch לכל האפליקציה. התוצאה היא ניהול State גלובלי מסודר, ללא ספרייה חיצונית - מעין "Redux מוקטן" מובנה:
const TodoContext = createContext();
function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<TodoContext.Provider value={{ todos, dispatch }}>
{children}
</TodoContext.Provider>
);
}
function useTodos() {
return useContext(TodoContext);
}
// שימוש
function TodoList() {
const { todos, dispatch } = useTodos();
// ...
}
סיכום
| מושג | הסבר |
|---|---|
useReducer |
Hook לניהול state מורכב |
reducer |
פונקציה (state, action) => newState |
dispatch |
פונקציה לשליחת actions |
action |
אובייקט עם type ו-payload |
// תבנית בסיסית
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'ACTION_TYPE', payload: data });
הקודם: ← Lazy Loading
הבא: Redux Toolkit →
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מתי useReducer עדיף על useState?
הצגת התשובה
תשובה ד. כל הלוגיקה מרוכזת בפונקציה אחת, קלה לבדיקה.
-
מה reducer חייב להחזיר?
הצגת התשובה
תשובה ב. reducer הוא פונקציה טהורה: (state, action) => newState.