useReducer — это хук, который использует редюсер для управления состоянием компонента.

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

Справочник

useReducer(reducer, initialArg, init?)

Вызовите useReducer на верхнем уровне компонента, чтобы управлять состоянием с помощью редюсера.

import { useReducer } from 'react';

function reducer(state, action) {
// ...
}

function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...

Больше примеров ниже.

Параметры

  • reducer: Редюсер — чистая функция, которая определяет логику обновления состояния. Редюсер принимает два аргумента – состояние и действие, и возвращает следующее состояние. Состояние и действие могут быть любых типов.
  • initialArg: Значение на основе которого вычисляется начальное состояние. Значение начального состояния может быть любого типа. То как из него будет вычисляться начальное состояние, зависит от аргумента init.
  • optional init: Функция инициализатора, которая возвращает начальное состояние. Если она не указана, то начальное состояние устанавливается в initialArg. В противном случае начальное состояние устанавливается в результат вызова init(initialArg).

Возвращаемое значение

useReducer возвращает массив, который содержит только два значения:

  1. Текущее состояние. Во время первого рендеринга устанавливается init(initialArg) или initialArg, если параметр init не указан.
  2. Функцию dispatch, которая обновляет состояние до другого значения и вызывает повторный рендеринг.

Замечания

  • `useReducer — это хук, поэтому вызывайте его только на верхнем уровне компонента или собственных хуков. useReducer нельзя вызвать внутри циклов или условий. Если это нужно, создайте новый компонент и переместите состояние в него.
  • В строгом режиме React будет вызывать редюсер и инициализатор дважды, чтобы помочь обнаружить случайные побочные эффекты. Такое поведение проявляется только в режиме разработки и не влияет на продакшен-режим. Логика обновления состояния не изменится, если редюсер и инициализатор – чистые функции (какими они и должны быть). Результат второго вызова проигнорируется.

Функция dispatch

Функция dispatch, которую возвращает useReducer, обновляет состояние до другого значения и вызывает повторный рендеринг. Передайте действие в качестве единственного аргумента функции dispatch:

const [state, dispatch] = useReducer(reducer, { age: 42 });

function handleClick() {
dispatch({ type: 'incremented_age' });
// ...

React установит следующее состояние как результат вызова функции reducer, которую вы предоставляете с текущим state и действием, которое вы передали в dispatch.

Параметры

  • action: Действие, выполняемое пользователем. action может быть значением любого типа. По соглашению action — объект со свойством type, идентифицирующим его, и, по желанию, другими свойствами с дополнительной информацией.

Returns

Функция dispatch не возвращает значения.

Замечания

  • Функция dispatch обновляет состояние только для следующего рендера. Если прочитать переменную состояния после вызова функции dispatch, вы получите старое значение, которое было на экране до вашего вызова.

  • Если новое значение, которое вы предоставили, идентично текущему state, что определяется сравнением Object.is, React пропустит повторное отображение компонента и дочерних элементов. Это оптимизация. React также может попытаться вызвать компонент перед игнорированием результата, но это не должно повлиять на код.

  • Пакетное обновление состояния React. Обновляет экран после того, как все обработчики событий были запущены и вызвали свои set функции. Это предотвратит множественные повторные рендеринги во время одного события. В редких случаях, когда нужно заставить React обновить экран раньше, например, для доступа к DOM, используйте flushSync.


Применение

Добавление редюсера в компонент

Вызовите useReducer на верхнем уровне компонента, чтобы управлять состоянием компонента с помощью редюсера.

import { useReducer } from 'react';

function reducer(state, action) {
// ...
}

function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...

useReducer возвращает массив, который состоит из двух элементов:

  1. Текущее состояние этой переменной состояния, первоначально установленное в начальное состояние, которое вы предоставили.
  2. Функция dispatch, которая позволяет менять состояние в ответ на взаимодействие.

Чтобы обновить то, что вы видите на экране, вызовите dispatch с объектом, представляющим то, что сделал пользователь, который называется action:

function handleClick() {
dispatch({ type: 'incremented_age' });
}

React передаст текущее состояние и действие в редюсер. Редюсер вычислит и вернёт следующее состояние. React сохранит это состояние, отрисует компонент и обновит пользовательский интерфейс.

import { useReducer } from 'react';

function reducer(state, action) {
  if (action.type === 'incremented_age') {
    return {
      age: state.age + 1
    };
  }
  throw Error('Unknown action.');
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });

  return (
    <>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })
      }}>
        Добавить год к возрасту
      </button>
      <p>Привет! Тебе {state.age}.</p>
    </>
  );
}

useReducer похож на useState, но он переносит логику обновления состояния из обработчиков событий в одну функцию вне компонента. Подробнее о выборе между useState и useReducer.


Составление функции редюсера

Объявите редюсер следующим образом:

function reducer(state, action) {
// ...
}

Затем, напишите код, который вычислит и возвратит следующее состояние. По традиции это делают при помощи инструкции switch. Для каждого case в switch вычислите и возвратите следующее состояние.

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}

Действия могут иметь любую форму. По традиции принято передавать объекты со свойством type, идентифицирующим действие. Оно должно включать минимально необходимую информацию, которая нужна редюсеру для вычисления следующего состояния.

function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });

function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}

function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...

Имена типов действий являются локальными для вашего компонента. Каждое действие описывает одно взаимодействие, даже если оно приводит к нескольким изменениям данных. Форма состояния произвольна, но обычно это будет объект или массив.

Прочитайте про извлечение логики состояния в редюсер чтобы узнать больше.

Pitfall

Состояние доступно только для чтения. Не изменяйте никакие объекты или массивы в состоянии:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Не изменяйте состояние объекта напрямую, подобно этому:
state.age = state.age + 1;
return state;
}

Вместо этого всегда возвращайте новые объекты из вашего редюсера:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ вместо этого верните новый объект:
return {
...state,
age: state.age + 1
};
}

Прочитайте про обновление объектов в состоянии и обновление массивов в состоянии, чтобы узнать больше.

Основные примеры использования useReducer

Example 1 of 3:
Форма (объект)

В этом примере редюсер управляет состоянием объекта с двумя полями: name и age.

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

const initialState = { name: 'Тэйлор', age: 42 };

export default function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    }); 
  }

  return (
    <>
      <input
        value={state.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Добавить год возраста
      </button>
      <p>Привет, {state.name}. Тебе {state.age}.</p>
    </>
  );
}


Избегание пересоздания начального состояния

React сохраняет начальное состояние один раз и игнорирует его при последующих рендерах.

function createInitialState(username) {
// ...
}

function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...

Хотя результат createInitialState(username) используется только для начального рендеринга, вы всё равно вызываете эту функцию при каждом рендеринге. Это может быть расточительно, если она создаёт большие массивы или выполняет дорогостоящие вычисления.

Чтобы решить эту проблему, вы можете передать её в качестве функции initializer, чтобы вместо него в качестве третьего аргумента использовать useReducer:

function createInitialState(username) {
// ...
}

function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...

Обратите внимание, что вы передаёте createInitialState, которая сама является функцией, а не createInitialState(), что является результатом её вызова. Таким образом, начальное состояние не будет повторно создано после инициализации.

В приведённом выше примере createInitialState принимает аргумент username. Если вашему инициализатору не нужна информация для вычисления начального состояния, вы можете передать null в качестве второго аргумента useReducer.

Разница между передачей инициализатора и начального состояния напрямую

Example 1 of 2:
Передача функции инициализатора

В этом примере передаётся функция инициализатора, поэтому функция createInitialState выполняется только во время инициализации. Она не выполняется при повторном рендеринге компонента, например, когда вы вводите текст в поле ввода.

import { useReducer } from 'react';

function createInitialState(username) {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: username + "'s task #" + (i + 1)
    });
  }
  return {
    draft: '',
    todos: initialTodos,
  };
}

function reducer(state, action) {
  switch (action.type) {
    case 'changed_draft': {
      return {
        draft: action.nextDraft,
        todos: state.todos,
      };
    };
    case 'added_todo': {
      return {
        draft: '',
        todos: [{
          id: state.todos.length,
          text: state.draft
        }, ...state.todos]
      }
    }
  }
  throw Error('Unknown action: ' + action.type);
}

export default function TodoList({ username }) {
  const [state, dispatch] = useReducer(
    reducer,
    username,
    createInitialState
  );
  return (
    <>
      <input
        value={state.draft}
        onChange={e => {
          dispatch({
            type: 'changed_draft',
            nextDraft: e.target.value
          })
        }}
      />
      <button onClick={() => {
        dispatch({ type: 'added_todo' });
      }}>Add</button>
      <ul>
        {state.todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}


Устранение неполадок

Я отправил действие, но возвращается старое значение состояния

Вызов функции dispatch не изменяет состояние в вызываемом коде:

function handleClick() {
console.log(state.age); // 42

dispatch({ type: 'incremented_age' }); // Запрос повторного рендеринга с 43
console.log(state.age); // Все ещё 42!

setTimeout(() => {
console.log(state.age); // Так же 42!
}, 5000);
}

Это происходит, потому что состояние ведёт себя как снимок. Обновление состояния запрашивает другой рендер с новым значением состояния, но не влияет на JavaScript-переменную state в уже запущенном обработчике событий.

Если вам нужно получить значение следующего состояния, вы можете вычислить его вручную, вызвав редюсер самостоятельно:

const action = { type: 'incremented_age' };
dispatch(action);

const nextState = reducer(state, action);
console.log(state); // { возраст: 42 }
console.log(nextState); // { возраст: 43 }

Я отправил действие, но экран не обновляется

React будет игнорировать ваше обновление, если следующее состояние равно предыдущему, что определяется инструкцией Object.is сравнения. Обычно это происходит, когда вы изменяете объект или массив в состоянии напрямую:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Неправильно: изменение существующего объекта
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Неправильно: изменение существующего объекта
state.name = action.nextName;
return state;
}
// ...
}
}

Вы изменили существующий объект state и вернули его, поэтому React проигнорировал обновление. Чтобы исправить это, убедитесь, что вы всегда обновляете объекты в состоянии и обновляете массивы в состоянии, не изменяя их:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Правильно: создание нового объекта
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Правильно: создание нового объекта
return {
...state,
name: action.nextName
};
}
// ...
}
}

Часть состояния моего редюсера становится неопределённой после диспетчеризации

Убедитесь, что каждая ветвь case копирует все существующие поля, когда возвращает новое состояние:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Не забывайте об этом!
age: state.age + 1
};
}
// ...

Без ...state выше, следующее состояние будет возвращать только поле age и ничего больше.


Все состояние моего редюсера становится неопределённым после диспетчеризации

Если ваше состояние неожиданно становится undefined, скорее всего, вы забыли возвратить состояние в одном из случаев, или ваш тип действия не соответствует ни одному из утверждений case. Чтобы выяснить причину, выбросьте ошибку вне case:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}

Вы также можете использовать статическую проверку типов, например TypeScript, для выявления таких ошибок.


Я получаю ошибку: “Too many re-renders”

Вы можете получить ошибку, которая гласит: Слишком много повторных рендеров. React ограничивает количество рендеров для предотвращения бесконечного цикла. Обычно это означает, что вы безоговорочно отправляете действие во время рендера, поэтому ваш компонент попадает в цикл: рендер, диспетчеризация (которая вызывает рендер), рендер, диспетчеризация (которая вызывает рендер) и так далее. Очень часто причиной этого является ошибка в определении обработчика события:

// 🚩 Неправильно: вызывает обработчик во время рендеринга
return <button onClick={handleClick()}>Click me</button>

// ✅ Правильно: передаёт обработчик события
return <button onClick={handleClick}>Click me</button>

// ✅ Правильно: передаётся через встроенную функцию
return <button onClick={(e) => handleClick(e)}>Click me</button>

Если вы не можете найти причину этой ошибки, нажмите на стрелку рядом с ошибкой в консоли и просмотрите стек JavaScript, чтобы найти конкретный вызов функции dispatch, ответственный за ошибку.


Моя функция редюсера или инициализатора выполняется дважды

В строгом режиме React будет вызывать ваши функции, редюсер и инициализатор, дважды. Это не должно сломать ваш код.

Это поведение справедливо только для режима разработки и помогает вам поддерживать чистоту компонентов. React использует результат одного из вызовов и игнорирует результат другого вызова. Пока ваши функции компонента, инициализатора и редюсера чисты, это не должно влиять на вашу логику. Однако если они случайно оказались нечистыми, это поможет вам заметить ошибки.

Например, эта нечистая функция редюсера изменяет массив состояния напрямую:

function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Ошибка: изменения состояния напрямую
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}

Поскольку React дважды вызывает вашу функцию reducer, вы увидите, что todo было добавлено дважды, поэтому вы будете знать, что произошла ошибка. В этом примере вы можете исправить ошибку, заменив массив вместо его изменения:

function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Правильно: замена на новое состояние
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}

Теперь, когда функция редюсера является чистой, её повторный вызов не изменит поведение. Вот почему двойной вызов в React помогает найти ошибки. Только функции компонента, инициализатора и редюсера должны быть чистыми. Обработчики событий не должны быть чистыми, поэтому React никогда не будет вызывать обработчики событий дважды.

Прочитайте про сохранение чистоты компонентов, чтобы узнать больше.