Как стать автором
Обновить
1300.89
OTUS
Цифровые навыки от ведущих экспертов

React useReducer, зачем нужен и как использовать

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров20K

useReducer - это хук для работы с состоянием компонента. Он используется под капотом у хука useState. В этой статье разберемся с api useReducer, когда лучше использовать useReducer вместо useState и поговорим про нестандартный случай использования useReducer.

useReducer api

useReducer - это хук для работы с состоянием компонента, как уже говорил выше. Чтобы его использовать, необходимо написать чистую функцию reducer(редуктор). Также useReducer принимает от 2-х до 3-х аргументов.

В минимальном рабочем варианте, он принимает reducer и начальное значение состояния.

Функция reducer, в свою очередь, принимает два аргумента: предыдущее состояние и экшн (действие) и обязательно возвращает новое состояние.

import React, { FC, useReducer } from 'react';

export type BaseExampleProps = {
  className?: string;
};

type State = {
  text: string;
};

type Action = {
  type: 'test';
};

// Чистая функция, принимает предыдущее значение состояния и экшн, с помощью
// которого изменим состояние
const reducer = (state: State, action: Action): State => {
  const { type } = action;
  switch (type) {
    case 'test':
      return { ...state, text: 'test' };

    default:
      return state;
  }
};

export const BaseExample: FC<BaseExampleProps> = () => {
  const [store, dispatch] = useReducer(reducer, { text: '' });
  return (
    <div>
      <div>{state.text}</div>
      <div>
        <button type="button" onClick={() => dispatch({ type: 'test' })}>
          test
        </button>
      </div>
    </div>
  );
};

Данный хук использует концепцию, схожую с flux архитектурой (об этом ниже). Редуктор (reducer) принимает предыдущее состояние и экшн, в котором есть обязательное поле type и необязательное payload.

type StandartAction = {
  type: string;
  payload?: any;
}

Классический редуктор выглядит как-то так:

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + action.payload };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

В случае выше тип экшн будет выглядеть вот так:

type Action = {
  type: 'INCREMENT';
  payload: number;
} | {
  type: 'DECREMENT';
}

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

Пара слов про архитектуру Flux

Архитектура Flux - это паттерн управления состоянием, разработанный Facebook для создания масштабируемых и управляемых приложений на React. Он был создан в ответ на проблемы, возникающие при управлении состоянием в сложных приложениях.

Основные компоненты архитектуры Flux:

1. Действия (Actions): Действия представляют собой объекты, которые описывают события или изменения, происходящие в приложении. Они инициируются различными событиями, такими как пользовательские действия, сетевые ответы и т. д. Действия содержат информацию о типе события и необходимых данных для обработки этого события.

2. Диспетчер (Dispatcher): Диспетчер является центральным хабом в архитектуре Flux. Он принимает действия и передает их зарегистрированным обработчикам (stores). Диспетчер также гарантирует, что действия обрабатываются последовательно и синхронно, что позволяет избежать состояния гонки.

3. Хранилища (Stores): Хранилища содержат состояние приложения и логику его обновления. Они реагируют на действия, обновляют состояние и уведомляют своих подписчиков о изменениях. Каждое хранилище отвечает за управление определенной частью данных приложения и имеет строгую структуру состояния.

4. Представления (Views): Представления (компоненты) React отображают состояние приложения и реагируют на его изменения. Они подписываются на хранилища для получения обновлений и перерисовки себя при изменении состояния. Представления также инициируют действия при пользовательском взаимодействии.

5. Единство данных (Unidirectional Data Flow): Flux использует однонаправленный поток данных, где изменения состояния могут происходить только путем действий, передаваемых через диспетчер и обрабатываемых хранилищами. Это упрощает отслеживание и отладку потоков данных в приложении.

Преимущества Flux:

  • Четкая структура: Flux обеспечивает четкую организацию кода и распределение ответственности между различными компонентами.

  • Отсутствие зависимостей: Компоненты в Flux взаимодействуют друг с другом через действия и хранилища, что позволяет избежать сложностей связанности.

  • Легкая отладка: Однонаправленный поток данных и строгий контроль изменения состояния упрощают обнаружение и исправление ошибок в коде.

  • Масштабируемость: Flux облегчает масштабирование приложений, поскольку разделение ответственности между компонентами позволяет эффективно управлять состоянием и логикой приложения.

В целом, архитектура Flux предлагает структурированный подход к управлению состоянием в React-приложениях, упрощая разработку и поддержку сложных приложений.

Результат вызова useReducer

useReducer, как и любой хук, является функцией. Этот хук возвращает массив с двумя элементами. Первый элемент массива - состояние, второй элемент - функция изменения состояния. Классически именуют так:

  • store либо state (состояние)

  • dispatch (функция изменения состояния)

const [store, dispatch] = useReducer(...);

Обращаю внимание, что деструктуризация массивов позволяет именовать как угодно, например так

const [count, increment] = useReducer(...);

Связь reducer и dispatch

Dispatch (функция, изменяющая состояние), использует reducer под капотом. Выглядит это примерно так:

const dispatch = (action) => {
  reducer(state, action);
};

Другими словами, вся логика описанная внутри reducer, будет выполнена при вызове dispatch, а единственный аргумент, который принимает dispatch и будет action.

Как не обновлять компонент при вызове dispatch. В react 18 не работает!

Бывают ситуации, когда при определенных условиях обновлять компонент не надо. Эту логику можно обработать в редукторе (reducer), но есть нюансы.

Чтобы не обновлять компонент в reducer нужно вернуть предыдущее состояние. Но это работает только для react 17 версии! В react 18 версии компонент будет обновлен в любом случае при вызове dispatch. На текущий момент актуальная версия react 18.0.2.

const reducer = (state, action) => {
  switch (action.type) {
    ...
    default:
      // в react17 компонент не будет обновлен
      // в react18 компонент будет обновлен
      return state;
  }
};

Следить за исправлением (если оно будет) здесь. Разработчики уверяют, что это не приведет к проблемам с производительностью (под вопросом).

Аргументы useReducer

Как говорилось выше, useReducer принимает от 2-х до 3-х аргументов.

Если заглянуть в типизацию этого хука, можно увидеть несколько вариантов, ужаснуться и закрыть.

Но на самом деле, здесь нет ничего сложного. Давайте разбираться.

Функция редуктор (reducer) в любом случае будет первым аргументом.

Вторым аргументом будут некоторые данные (initialiserArg или initialState). Обязательный. Если мы передали только два аргумента - эти данные будут начальным состоянием. Если передали три аргумента - эти данные будут переданы в третий аргумент useReducer.

Третий аргумент необязательный (initialiser) - это функция, которая будет вызвана единственный раз при монтировании компонента и она должна вернуть начальное значение состояния. Как говорилось выше, эта функция в качестве своего аргумента получит второй аргумент useReducer, это позволяет функцию initialiser вынести из компонента. Смысл этой функции в оптимизации. Если начальное состояние нужно вычислить, лучше использовать эту функцию, иначе при каждом обновлении компонента будете вычислять начальное состояние, но никак его не использовать.

useReducer(reducer, initialState);
useReducer(reducer, undefined, () => initialState);
useReducer(reducer, arg, (arg) => initialState);

Когда использовать useReducer

Не существует строгого правила, когда нужно использовать useState, а когда useReducer. Но есть некоторые признаки, по которым можно понять, что стоит попробовать useReducer.

  • Когда в компоненте есть несколько useState, в целях оптимизации можно заменить их единственным useReducer. Меньше хуков, меньше затрат памяти, больше производительность. Однако это зависит от приоритетов. useReducer может потребовать написать много action и это может негативно сказаться на читабельности кода.

  • Если одно состояние зависит от другого, это с большой вероятностью работа для useReducer. Все зависимости одного состояния от другого лучше описывать в редукторе (reducer)

const [state1, setState1] = useState();
const [state2, setState2] = useState();

// В данном случае лучше использовать useReducer
useEffect(() => {
  if (state1 === any) setState2();
}, []);

Нестандартное использование

Выше мы обсуждали, что useReducer основан на коцепции flux и существует договоренность, что в dispatch передаем action типа { type: string; payload: any }. Однако технически нет никаких ограничений использовать другие данные и ниже хочу показать два варианта использования useReducer для частых кейсов.

Переключатель с использованием useReducer

Часто нужно создать переключатель boolean состояния (visible например). В случае, если нам нужна замемоизированная функция переключения (toggleVisible) код будет выглядеть следующим образом.

const [visible, setVisible] = useState(false);

const toggleVisible = useCallback(() => setVisible(v => !v));

Однако этот код можно сократить до одной строчки с помощью useReducer

const [visible, toggleVisible] = useReducer((v) => !v, false);

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

Счетчик с использованием useReducer

Аналогично, как и в примере выше, можно сделать увеличение счетчика.

const [count, setCount] = useState(0);
const increment = useCallback(() => setCount(v => v + 1));

C помощью useReducer

const [count, increment] = useReducer((v) => v + 1, 0);

Также можно сделать увеличение счетчика на заданное количество.

const [count, increment] = useReducer((v, amount = 1) => v + amount, 0);

increment() // увеличит на 1
increment(10) // увеличит на 10
increment(-10) // уменьшит на 10

onChange на useReducer

Часто нужно писать подобный код

const [value, setValue] = useState('');
const onChange = useCallback((e: React.ChangeEvent) => setValue(e.target.value));

Этот код также можно написать с использованием useReducer

const [value, onChange] = useReducer((_, e) => e.target.value);

Итоги

useReducer - это истинный хук изменения состояния (useState под капотом использует useReducer). Данный хук основывается на архитектуре flux, поэтому принимает редуктор (reducer) и предоставляет dispatch (функция изменения состояния), который в свою очередь принимает action.

Если в коде есть несколько useState и одно состояние зависит от другого - это верный признак, что лучше использовать useReducer. Можно отойти от классического использования  useReducer и использовать для создания переключателей, счетчиков, состояния инпутов и вообще всего, на что хватит фантазии.

Напоследок хочу пригласить всех желающих на бесплатный урок курса React.js Developer. Фуллстек разработка с SSR никогда не была такой простой и доступной! На уроке вы научитесь бутстрапить полноценные легко развертываемые приложения с клиентской и серверной частью. На примере разберем настройку сборки, процесс разработки и развертывания приложения. Вы получите удобный набор для старта разработки любого веб-приложения на современном стеке.

Теги:
Хабы:
Всего голосов 10: ↑10 и ↓0+10
Комментарии12

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS