useReducer: сложное состояние
Когда useState не хватает
Пока состояние — одно-два значения, useState идеален. Но в настоящих приложениях логика быстро
разрастается: список задач, который можно добавлять, отмечать, удалять, фильтровать, очищать. Код
изменения состояния расползается по десятку обработчиков в разных местах компонента, и уже трудно
понять, как вообще может меняться состояние.
useReducer собирает всю логику изменений в одну функцию — редьюсер. Обработчики событий
больше не говорят «как изменить», они только сообщают, что произошло.
Действия и редьюсер
function tasksReducer(tasks, action) {
switch (action.type) {
case "added":
return [...tasks, { id: action.id, text: action.text, done: false }];
case "toggled":
return tasks.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
case "deleted":
return tasks.filter((t) => t.id !== action.id);
default:
throw new Error("Unknown action: " + action.type);
}
}
- Действие (action) — обычный объект, который описывает событие. По соглашению у него есть поле
type, остальное — данные:{ type: "deleted", id: 3 }. - Редьюсер (reducer) — функция
(текущееСостояние, действие) → новоеСостояние. Название — от метода массивовreduce: он тоже «сворачивает» последовательность в одно значение.
Редьюсер можно проверить без React, как обычную функцию:
let state = [];
state = tasksReducer(state, { type: "added", id: 1, text: "Купить хлеб" });
state = tasksReducer(state, { type: "added", id: 2, text: "Позвонить маме" });
state = tasksReducer(state, { type: "toggled", id: 1 });
console.log(state);
useReducer в компоненте
const { useReducer } = React;
function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, []);
return (
<div>
<button onClick={() => dispatch({ type: "added", id: Date.now(), text: "Новая" })}>
Добавить
</button>
<ul>
{tasks.map((t) => (
<li key={t.id}>
<span onClick={() => dispatch({ type: "toggled", id: t.id })}>{t.text}</span>
<button onClick={() => dispatch({ type: "deleted", id: t.id })}>×</button>
</li>
))}
</ul>
</div>
);
}
useReducer(редьюсер, начальноеСостояние)возвращает пару: текущее состояние и функциюdispatch.dispatch(действие)— «отправить действие». React вызовет редьюсер с текущим состоянием и этим действием, сохранит результат и перерисует компонент.
Сравни с useState: там обработчик сам решает, каким будет новое состояние. Здесь обработчик
только сообщает о событии, а решает редьюсер.
Правила для редьюсера
Редьюсер должен быть чистой функцией:
- Не менять входное состояние — возвращать новый массив или объект (как с
useState, иначе React не увидит изменений). - Не делать побочных эффектов: никаких запросов, таймеров,
Math.random()илиDate.now()внутри. Случайный id лучше сгенерировать в обработчике и передать в действии. - Для неизвестного действия — выбросить ошибку. Опечатка в
typeсразу станет заметна.
Чистота даёт большой плюс: результат зависит только от входа, поэтому редьюсер легко тестировать, а поведение приложения можно воспроизвести, повторив те же действия.
Если в case нужно объявить переменную, оберни его в фигурные скобки — так переменные разных
веток не будут конфликтовать:
case "added": {
const id = Math.max(0, ...tasks.map((t) => t.id)) + 1;
return [...tasks, { id, text: action.text, done: false }];
}
useState или useReducer?
useState |
useReducer |
|
|---|---|---|
| Сколько кода | меньше | больше (редьюсер + действия) |
| Простые значения (флаг, текст поля) | ✓ | избыточно |
| Много способов изменить одно состояние | обработчики расползаются | вся логика в одном месте |
| Тестирование логики | вместе с компонентом | редьюсер — отдельно, как функция |
| Отладка | — | можно вывести каждое действие в консоль |
Хорошее правило: начинай с useState. Если обработчиков, меняющих одно и то же состояние,
становится много — переходи на useReducer. Это просто сделать: действия описывают то, что и
так происходит в обработчиках.
Редьюсер + контекст
useReducer и useContext вместе — стандартная схема для состояния всего приложения без
внешних библиотек:
const TasksContext = createContext(null);
const DispatchContext = createContext(null);
function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, []);
return (
<TasksContext.Provider value={tasks}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</TasksContext.Provider>
);
}
function DeleteButton({ id }) {
const dispatch = useContext(DispatchContext);
return <button onClick={() => dispatch({ type: "deleted", id })}>×</button>;
}
- Состояние и логика — в одном месте (
TasksProvider+ редьюсер). - Любой компонент на любой глубине читает задачи (
TasksContext) или отправляет действия (DispatchContext), без props. - Почему два контекста? Функция
dispatchникогда не меняется, поэтому компоненты, которым нужен толькоdispatch(кнопки), не будут перерисовываться при каждом изменении списка.
Похожим образом устроены популярные библиотеки состояния — Redux и Zustand: хранилище, действия и
чистые функции обновления. Поняв useReducer, ты поймёшь и их.
Итог
useReducer(reducer, initial)→[state, dispatch].- Действие — объект
{ type, ...данные }, описывает что произошло. - Редьюсер — чистая функция
(state, action) => newState: без мутаций и эффектов, неизвестное действие — ошибка. useState— для простого,useReducer— когда способов изменить состояние много.- Редьюсер + два контекста (состояние и
dispatch) — состояние всего приложения.