Урок 08

useReducer: сложное состояние

useReducer — альтернатива useState для сложного состояния. Логика обновления выносится в отдельную функцию-редьюсер.

Синтаксис

const [state, dispatch] = useReducer(reducer, initialState);
  • state — текущее состояние
  • dispatch — функция для отправки действий
  • reducer — функция, которая обрабатывает действия и возвращает новое состояние

Редьюсер

type State = {
    count: number;
};

type Action =
    | { type: 'increment' }
    | { type: 'decrement' }
    | { type: 'reset' }
    | { type: 'set'; payload: number };

function reducer(state: State, action: Action): State {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        case 'reset':
            return { count: 0 };
        case 'set':
            return { count: action.payload };
        default:
            return state;
    }
}

Использование

function Counter() {
    const [state, dispatch] = useReducer(reducer, { count: 0 });

    return (
        <>
            <p>{state.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
            <button onClick={() => dispatch({ type: 'reset' })}>Сброс</button>
            <button onClick={() => dispatch({ type: 'set', payload: 10 })}>10</button>
        </>
    );
}

Почему лучше useState

Когда состояний много и они меняются вместе, useState превращается в кашу. Редьюсер даёт:

  • Вся логика обновления в одном месте
  • Действия явно названы
  • Легко добавить новые действия
  • Легко тестировать

Пример: форма

type State = {
    name: string;
    email: string;
    errors: Record<string, string>;
    isSubmitting: boolean;
};

type Action =
    | { type: 'setField'; field: string; value: string }
    | { type: 'setErrors'; errors: Record<string, string> }
    | { type: 'submit' }
    | { type: 'reset' };

function reducer(state: State, action: Action): State {
    switch (action.type) {
        case 'setField':
            return { ...state, [action.field]: action.value };
        case 'setErrors':
            return { ...state, errors: action.errors, isSubmitting: false };
        case 'submit':
            return { ...state, isSubmitting: true };
        case 'reset':
            return { name: '', email: '', errors: {}, isSubmitting: false };
    }
}

Ленивая инициализация

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

init — функция, которая получает initialArg и возвращает начальное состояние.

function init(initialCount: number) {
    return { count: initialCount };
}

useReducer(reducer, props.initialCount, init);

Когда использовать

  • Много связанных состояний
  • Следующее состояние зависит от предыдущего сложным образом
  • Логика обновления повторяется
  • Нужно тестировать логику отдельно

useState vs useReducer

  • useState — для простых значений
  • useReducer — для сложных структур и переходов

Если обновлений больше 3–4 и они взаимосвязаны — useReducer чище.

Итоги

  • useReducer — для сложного состояния
  • Логика в редьюсере
  • Действия через dispatch
  • Легко тестировать и расширять
  • Альтернатива useState, не замена