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