Spec-Zone.ru › React 18

useReducer

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

const [state, dispatch] = useReducer(reducer, initialArg, init?)
  • Справочник
    • useReducer(reducer, initialArg, init?)
    • Функция dispatch
  • Использование
    • Добавление редусера в компонент
    • Написание функции редусера
    • Избегание повторного создания начального состояния
  • Отладка
    • Отправил действие, но лог показывает старое значение состояния
    • Отправил действие, но экран не обновляется
    • Часть состояния редусера становится неопределенной после отправки
    • Всё состояние редусера становится неопределённым после отправки
    • Ошибка: «Слишком много перерисовок»
    • Функция редусера или инициализации выполняется дважды

Справочник

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.
  • необязательный init: Функция инициализации, которая должна возвращать начальное состояние. Если она не указана, начальное состояние устанавливается в initialArg. В противном случае начальное состояние устанавливается в результат вызова init(initialArg).

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

useReducer возвращает массив ровно с двумя значениями:

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

Ограничения

  • useReducer — хук, поэтому вы можете вызывать его только на верхнем уровне вашего компонента или в своих собственных хуках. Вы не можете вызывать его внутри циклов или условных операторов. Если вам это нужно, создайте новый компонент и переместите состояние в него.
  • Функция dispatch имеет стабильную идентичность, поэтому вы часто видите её исключённой из зависимостей Effect, но включение её не заставит Effect срабатывать. Если линтер позволяет опустить зависимость без ошибок, это безопасно. Узнайте больше об исключении зависимостей Effect.
  • В режиме Strict Mode React вызовет ваш редусер и функцию инициализации дважды, чтобы помочь вам найти случайные ошибки. Это поведение только для разработки и не влияет на производство. Если ваш редусер и функция инициализации чистые (как они должны быть), это не должно повлиять на вашу логику. Результат одного из вызовов игнорируется.

Функция dispatch

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

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

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

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

Параметры

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

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

Функции 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 с объектом, представляющим действия пользователя, называемым действием:

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' })
      }}>
        Increment age
      </button>
      <p>Hello! You are {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
    });
  }
  // ...

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

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

Подводные камни

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

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // 🚩 Don't mutate an object in state like this:
      state.age = state.age + 1;
      return state;
    }

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

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // ✅ Instead, return a new object
      return {
        ...state,
        age: state.age + 1
      };
    }

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


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

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

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

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

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

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

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

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

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

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


Отладка

Отправил действие, но лог показывает старое значение состояния

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

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

  dispatch({ type: 'incremented_age' }); // Request a re-render with 43
  console.log(state.age);  // Still 42!

  setTimeout(() => {
    console.log(state.age); // Also 42!
  }, 5000);
}

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

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

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

const nextState = reducer(state, action);
console.log(state);     // { age: 42 }
console.log(nextState); // { age: 43 }

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

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

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // 🚩 Wrong: mutating existing object
      state.age++;
      return state;
    }
    case 'changed_name': {
      // 🚩 Wrong: mutating existing object
      state.name = action.nextName;
      return state;
    }
    // ...
  }
}

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

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      // ✅ Correct: creating a new object
      return {
        ...state,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      // ✅ Correct: creating a new object
      return {
        ...state,
        name: action.nextName
      };
    }
    // ...
  }
}

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

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

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        ...state, // Don't forget this!
        age: state.age + 1
      };
    }
    // ...

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


Всё состояние моего редьюсера становится неопределённым после отправки действия

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

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 limits the number of renders to prevent an infinite loop. Обычно это означает, что вы безусловно отправляете действие во время отрисовки, поэтому ваш компонент попадает в цикл: отрисовка, отправка (что приводит к отрисовке), отрисовка, отправка (что приводит к отрисовке) и так далее. Очень часто это вызвано ошибкой в описании обработчика событий:

// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>

// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>

// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>

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


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

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

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

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

function reducer(state, action) {
  switch (action.type) {
    case 'added_todo': {
      // 🚩 Mistake: mutating state
      state.todos.push({ id: nextId++, text: action.text });
      return state;
    }
    // ...
  }
}

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

function reducer(state, action) {
  switch (action.type) {
    case 'added_todo': {
      // ✅ Correct: replacing with new state
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: nextId++, text: action.text }
        ]
      };
    }
    // ...
  }
}

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

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

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/reference/react/useReducer

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API