От автора: если вы использовали хук useState() для управления нетривиальным состоянием, например списком элементов, где вам нужно добавлять, обновлять и удалять элементы в состоянии, вы могли заметить, что логика управления состоянием занимает значительную часть тела компонента.
Это проблема, потому что компонент React по своей природе должен содержать логику, которая вычисляет результат. Но логика управления состоянием — это иная задача, которой нужно управлять в отдельном месте. В противном случае вы получите сочетание управления состоянием и логики рендеринга в одном месте, и ваш код будет сложно читать, поддерживать и тестировать!
Чтобы помочь вам разделить задачи (рендеринг и управление состоянием), React предоставляет хук useReducer(). Хук делает свою работу, извлекая управление состоянием из компонента.
Посмотрим, как работает хук useReducer(). В качестве приятного бонуса, в этом посте, вы найдете реальный пример, который поможет понять, как работает reducer.
1. useReducer()
Хук useReducer(reducer, initialState) принимает 2 аргумента: функцию reducer и начальное состояние. Затем хук возвращает массив из двух элементов: текущее состояние и функцию dispatch.
1 2 3 4 5 6 7 8 9 10 11 12 |
import { useReducer } from 'react'; function MyComponent() { const [state, dispatch] = useReducer(reducer, initialState); const action = { type: 'ActionType' }; return ( <button onClick={() => dispatch(action)}> Click me </button> ); } |
Теперь давайте разберемся, что означают термины initial state, action object, dispatch, и reducer.
А. Initial state
Initial state является значением состояния инициализации. Например, в случае состояния счетчика начальное значение может быть:
1 2 3 4 |
// initial state const initialState = { counter: 0 }; |
B. Аction object
Объект действия — это объект, описывающий, как обновить состояние. Обычно объект действия имеет свойство type — строку, описывающую, какое обновление состояния должен выполнить reducer. Например, объект действия по увеличению счетчика может выглядеть следующим образом:
1 2 3 |
const action = { type: 'increase' }; |
Если объект действия должен нести некоторую полезную информацию (также известную как полезная нагрузка), которая будет использоваться reducer-ом, то вы можете добавить дополнительные свойства к объекту действия. Например, вот объект действия для добавления нового пользователя в массив состояний пользователей:
1 2 3 4 5 6 7 |
const action = { type: 'add', user: { name: 'John Smith', email: 'jsmith@mail.com' } }; |
user — это свойство, содержащее информацию о добавляемом пользователе.
C. Dispatch
dispatch — специальная функция, которая отправляет объект действия. Функция dispatch создается хуком useReducer():
1 |
const [state, dispatch] = useReducer(reducer, initialState); |
Всякий раз, когда вы хотите обновить состояние (обычно из обработчика события или после завершения выборки запроса), просто вызовите функцию dispatch для необходимого объекта действия: dispatch(actionObject).
D. Reducer
Reducer является функцией, которая принимает 2 параметра: текущее состояние и объект действия. В зависимости от объекта действия функция reducer должна обновлять состояние и возвращать новое. В следующем примере, функция reducer поддерживает увеличение и уменьшение состояния счетчика:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function reducer(state, action) { let newState; switch (action.type) { case 'increase': newState = { counter: state.counter + 1 }; break; case 'descrease': newState = { counter: state.counter - 1 }; break; default: throw new Error(); } return newState; } |
Приведенный выше reducer не изменяет напрямую текущее состояние переменной state, а создает новый объект состояния, хранящийся в newState, а затем возвращает его.
vReact проверяет разницу между новым и текущим состоянием, чтобы определить, было ли обновлено состояние, поэтому не изменяйте текущее состояние напрямую.
E. Объединение всего
Объединив все эти термины вместе, мы увидим как работает обновление состояния с помощью reducer.
В результате обработки события или после выполнения запроса на выборку вы вызываете функцию dispatch с action object. Затем React перенаправляет объект действия и текущее значение состояние в функцию reducer.
Функция reducer использует action object и выполняет обновление состояния, возвращая новое состояние.
Затем React проверяет, отличается ли новое состояние от предыдущего. Если состояние было обновлено, React повторно отрисовывает компонент и useReducer() возвращает новое значение состояния: [newState, …] = useReducer(…).
Обратите внимание, что дизайн useReducer() основан на архитектуре Flux.
Если все эти термины звучат слишком абстрактно, то у вас правильное предчувствие! Посмотрим, как работает useReducer() на интересном примере.
2. Реализация секундомера
Задача — реализовать секундомер. Секундомер имеет 3 кнопки: «Пуск», «Стоп» и «Сброс», а также число, отображающее прошедшие секунды. Теперь давайте подумаем о структурировании состояния секундомера.
Есть 2 важных свойства состояния: логическое значение, указывающее, работает ли секундомер (назовем его isRunning), и число, указывающее количество прошедших секунд (назовем его time). В результате вот как может выглядеть initial state:
1 2 3 4 |
const initialState = { isRunning: false, time: 0 }; |
Исходное состояние указывает, что секундомер запускается как неактивный со значением 0 секунд.
Затем давайте рассмотрим, какие action objects должен иметь наш секундомер. Легко обнаружить, что нам нужно 4 вида действий: запуск, остановка и сброс текущего процесса секундомера, а также отсчет времени каждую секунду.
1 2 3 4 5 6 7 8 |
// The start action object { type: 'start' } // The stop action object { type: 'stop' } // The reset action object { type: 'reset' } // The tick action object { type: 'tick' } |
Имея структуру состояния, а также возможные действия, давайте воспользуемся функцией reducer, чтобы определить, как action object обновляюет состояние:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function reducer(state, action) { switch (action.type) { case 'start': return { ...state, isRunning: true }; case 'stop': return { ...state, isRunning: false }; case 'reset': return { isRunning: false, time: 0 }; case 'tick': return { ...state, time: state.time + 1 }; default: throw new Error(); } } |
Наконец, вот компонент Stopwatch, который связывает все вместе, вызывая хук useReducer():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { useReducer, useEffect, useRef } from 'react'; function Stopwatch() { const [state, dispatch] = useReducer(reducer, initialState); const idRef = useRef(0); useEffect(() => { if (!state.isRunning) { return; } idRef.current = setInterval(() => dispatch({type: 'tick'}), 1000); return () => { clearInterval(idRef.current); idRef.current = 0; }; }, [state.isRunning]); return ( <div> {state.time}s <button onClick={() => dispatch({ type: 'start' })}> Start </button> <button onClick={() => dispatch({ type: 'stop' })}> Stop </button> <button onClick={() => dispatch({ type: 'reset' })}> Reset </button> </div> ); } |
Попробуйте демо.
Обработчики событий клика кнопок Start, Stop и Reset соответственно используют функцию dispatch() для отправки необходимого action object.
Внутри обратного вызова useEffect(), если state.isRunning равен true, функция таймера setInterval() отправляет объект действия тика каждую секунду dispatch({type: ‘tick’}).
Каждый раз, когда функция reducer() обновляет состояние, в результате компонент повторно отрисовывается и получает новое состояние.
3. Ментальная модель Reducer
Чтобы еще больше закрепить ваши знания, давайте посмотрим на реальный пример, который работает аналогично reducer. Представьте, что вы капитан корабля в первой половине 20 века.
На капитанском мостике есть специальное устройство связи — телеграф машинного приказа (см. рисунок выше). Этот инструмент связи используется для передачи команд с мостика в машинное отделение. Типичные команды — медленно двигаться назад, двигаться вперед на половину мощности, останавливаться и т. д.
Вы на мостике, а корабль полностью остановлен. Вы (капитан) хотите, чтобы корабль двигался вперед на полной скорости. Вы подходите к телеграфу управления двигателем и устанавливаете ручку вперед до упора. Инженеры в машинном отделении, имея такое же устройство, видят команду и устанавливают двигатель на соответствующий режим.
Телеграф управления двигателем является функцией dispatch, команды — action object, инженеры в машинном отделении — функцией reducer, а режим двигателя — состоянием.
Телеграф машинного управления помогает отделить мост от машинного отделения. Точно так же хук useReducer() помогает отделить рендеринг от логики управления состоянием.
4. Вывод
Хук React useReducer() позволяет отделить управление состоянием от рендеринга логики компонентов.
const [state, dispatch] = useReducer(reducer, initialState) принимает 2 аргумента: функцию reducer и initial state. Также редуктор возвращает массив из двух элементов: текущего состояния и функции dispatch.
Если вы хотите обновить состояние, просто вызовите dispatch(action) для соответствующего action object. Затем объект действия передается функции reducer(), которая обновляет состояние. Если состояние было обновлено, то компонент выполняет повторный рендеринг, и хук [state, …] = useReducer(…) возвращает новое значение состояния.
UseReducer() отлично подходит для относительно сложного обновления состояния (требующего как минимум 2-3 действий обновления). Для простого управления состоянием просто используйте useState().
Автор: Dmitri Pavlutin
Источник: dmitripavlutin.com
Редакция: Команда webformyself.
Читайте нас в Telegram, VK, Яндекс.Дзен