Перейти к содержимому
Тема 9 из 13
Теория

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: там обработчик сам решает, каким будет новое состояние. Здесь обработчик только сообщает о событии, а решает редьюсер.

Правила для редьюсера

Редьюсер должен быть чистой функцией:

  1. Не менять входное состояние — возвращать новый массив или объект (как с useState, иначе React не увидит изменений).
  2. Не делать побочных эффектов: никаких запросов, таймеров, Math.random() или Date.now() внутри. Случайный id лучше сгенерировать в обработчике и передать в действии.
  3. Для неизвестного действия — выбросить ошибку. Опечатка в 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) — состояние всего приложения.

Редьюсер счётчика

Лёгкая+10 XP

Напиши чистую функцию counterReducer(state, action). Состояние — объект { count }. Действия:

  • { type: "increment" } — плюс 1;
  • { type: "decrement" } — минус 1;
  • { type: "add", amount } — плюс amount;
  • { type: "reset" } — count становится 0;
  • любое другое — выбросить Error с сообщением Unknown action: <type>.

Функция возвращает новый объект и не меняет state.

counterReducer({ count: 5 }, { type: "increment" })        → { count: 6 }
counterReducer({ count: 5 }, { type: "add", amount: 10 })  → { count: 15 }
counterReducer({ count: 5 }, { type: "jump" })             → Error: Unknown action: jump

Подключи коннектор, чтобы решать задачи

Код задач выполняется на твоём компьютере — так быстрее и надёжнее, чем в браузере. Для этого нужна бесплатная программа-коннектор CodeWithMe. Подключить её нужно один раз.

Ищем коннектор на этом компьютере…

  1. 1
    Скачай коннектор для своей системы (Windows, macOS или Linux).Скачать коннекторНа странице загрузки выбери файл со своей системой в названии, например windows-amd64.
  2. 2Запусти его и выбери папку, где будут лежать твои решения. Python и Node.js он поставит туда сам, когда понадобится.
  3. 3
    Введи ниже код из 6 цифр — он написан в окне коннектора («Код для сайта CodeWithMe»). Если браузер спросит про доступ к локальной сети — разреши.

Коннектор открытый (исходный код на GitHub), работает только на твоём компьютере и выполняет код лишь после ввода кода сопряжения.