useReducer
useReducer
— это хук, который использует редюсер для управления состоянием компонента.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
- Справочник
- Применение
- Устранение неполадок
- Я отправил действие, но возвращается старое значение состояния
- Я отправил действие, но экран не обновляется
- Часть состояния моего редьюсера становится неопределенной после диспетчеризации
- Все состояние моего редьюсера становится неопределенным после диспетчеризации
- Я получаю ошибку: “Too many re-renders”
- Моя функция редьюсера или инициализатора выполняется дважды
Справочник
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
возвращает массив, который содержит только два значения:
- Текущее состояние. Во время первого рендеринга, устанавливается
init(initialArg)
илиinitialArg
если не указать параметрinit
. - Функцию
dispatch
function, которая обновляет состояние до другого значения и вызывает повротный рендеринг.
Замечания
- `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
возвращает массив, который состоит из двух элементов:
- Текущее состояние этой переменной состояния, первоначально установленное в начальное состояние, которое вы предоставили.
- Функция
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
});
}
// ...
Имена типов действий являются локальными для вашего компонента. Каждое действие описывает одно взаимодействие, даже если оно приводит к нескольким изменениям данных. Форма состояния произвольна, но обычно это будет объект или массив.
Прочитайте про извлечение логики состояния в редьюсер чтобы узнать больше.
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 будет вызывать ваши функции reducer и initializer дважды. Это не должно сломать ваш код.
Это поведение, справдедливо только для режима разработки, и помогает вам поддерживать чистоту компонентов. 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 никогда не будет вызывать обработчики событий дважды.
Прочитайте про сохранение чистоты компонентов чтобы узнать больше.