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

от автора

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 никогда не была такой простой и доступной! На уроке вы научитесь бутстрапить полноценные легко развертываемые приложения с клиентской и серверной частью. На примере разберем настройку сборки, процесс разработки и развертывания приложения. Вы получите удобный набор для старта разработки любого веб-приложения на современном стеке.


ссылка на оригинал статьи https://habr.com/ru/articles/752824/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *