useReducer

מבוא

useReducer הוא Hook לניהול State מורכב. הוא מבוסס על אותו רעיון כמו Redux: במקום לעדכן את ה-State ישירות, שולחים "פעולה" (action) לפונקציה אחת מרכזית - ה-reducer - שאחראית על כל לוגיקת העדכון. הוא חלופה ל-useState, ומתאים יותר כאשר:

  • ה-State מכיל כמה ערכים הקשורים זה לזה
  • לוגיקת העדכון מורכבת ומתפצלת למקרים רבים
  • העדכון הבא תלוי במצב הקודם

התחביר הבסיסי

const [state, dispatch] = useReducer(reducer, initialState);

הרכיבים:

  1. state - המצב הנוכחי
  2. dispatch - פונקציה לשליחת פעולות (actions)
  3. reducer - פונקציה שמחשבת את ה-state החדש
  4. 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 →

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מתי useReducer עדיף על useState?

    1. תמיד
    2. אף פעם
    3. לערך בוליאני אחד
    4. כשה-state מורכב ויש הרבה סוגי עדכונים שתלויים זה בזה
    הצגת התשובה

    תשובה ד. כל הלוגיקה מרוכזת בפונקציה אחת, קלה לבדיקה.

  2. מה reducer חייב להחזיר?

    1. undefined
    2. state חדש, בלי לשנות את הקודם
    3. פונקציה
    4. action
    הצגת התשובה

    תשובה ב. reducer הוא פונקציה טהורה: (state, action) => newState.