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

profesor08
07.08.2023 12:14+3React useReducer, зачем нужен и как использовать
Ну, нужен он самому реакту, это понятно. Но зачем его использовать у себя в коде, совсем не понятно. Описанные преимущества ценны в сравнении и наглядном примере. Но сравнений нет, а примеры неудачные. Потому что рекомендованный вариант превратит код в нечитабельный кусок г, а нестандартные не соответствуют друг другу, так как предоставляют разные возможности.

kugichka
07.08.2023 12:14Согласитесь, соблюдать баланс читабельности кода и производительности нужно. Автор привёл весьма удачный пример с useEffect: кейс когда значение одного useState зависит от значения другого useState и синхронизация происходит через useEffect -- действительно ест лишнее процессорное время и память. Так же, произвести все вычисления стейтов в одном месте (в reduce) и не размазывать логику по useEffect'ам -- должно быть "читабельнее" || "снижать когнитивную нагрузку" || "упрощать дебаг". К тому же Дэн Абрамов не отменяет useReducer.

profesor08
07.08.2023 12:14Это плохой пример, потому что такие ситуации должны решаться батчингом. Там, где устанавливается state1, должен устанавливаться и state2.
const newState1 = {...}
setState1(newState1)
if (newState1 === any) setState2();

markelov69
07.08.2023 12:14-1К тому же Дэн Абрамов не отменяет useReducer.
Ахахха, жесть вот это "логика". А если он скажет что лучше использовать голый js, а не реакт, то что?
Согласитесь, соблюдать баланс читабельности кода и производительности нужно
Разница в сколько-то микросекунд или даже миллисекунд не играет роли на клиентском приложении, когда за это приходится платить говнокодом. Гораздо правильнее отдать предпочтение хорошему коду, чем выйграть несколько микросекунд.
markelov69
useReducer - вообще не нужен, ни в каких сценариях. Использовать его не надо и вообще противопоказано. Просто используйте MobX в связке с реактом и забудьте о всех заботах и проблемах.