В своем посте Прекратите использовать логические значения isLoading Кент С. Доддс говорит нам… ну, прекратить использование логических значений для отслеживания двоичных состояний 😅. Проблема с использованием логического флага для моделирования состояния нашего компонента заключается в том, что обычно флаг является лишь частью более крупного и сложного состояния.
Когда вы кодируете состояние вашего компонента с помощью набора двоичных флагов, вы заканчиваете рендеринг с большим количеством условных проверок. Это приводит к коду, который действительно сложно поддерживать и расширять.
Вместо использования логических значений Кент рекомендует использовать конечный автомат. В своем посте он использует полноценную библиотеку конечных автоматов xstate.
В этой истории я предложу более простую альтернативу, реализованную в виде Custom Hook с использованием useReducer
.
Конечно, поскольку эта альтернатива проще, она также менее эффективна, поэтому, если вы создаете сложный компонент, попробуйте xstate!
Определение useStateMachine
Цитируя Википедию, конечный автомат (или конечный автомат, FSM) - это
[…] Абстрактная машина, которая может находиться ровно в одном из конечного числа состояний в любой момент времени. Автомат может переходить из одного состояния в другое в ответ на некоторые входные данные; переход из одного состояния в другое называется переходом. Автомат определяется списком его состояний, его начальным состоянием и входами, которые запускают каждый переход.
Итак, чтобы построить собственный конечный автомат, нам понадобятся:
- Список состояний.
- Список событий (входов).
- Список переходов (текущее состояние + событие = ›следующее состояние. Этот выглядит знакомым…).
Мы собираемся начать разработку API для нашего useStateMachine
пользовательского хука. Нам нужно что-то вроде этого:
const [currentState, sendEvent] = useStateMachine(machineSpec);
Hook принимает объект спецификации машины, определяющий состояния, входы и переходы, и возвращает текущее состояние и метод для отправки события на машину, чтобы заставить ее измениться (перейти) в новое состояние.
Скажем, мы хотим использовать конечный автомат для обработки подключения к некоторой удаленной службе. Наш пользователь начинает отключаться, и мы хотим, чтобы он подключился к услуге и отключился от нее. Мы можем представить эту машину так:
Спецификация кодирует всю информацию, необходимую для запуска машины:
- Список состояний, соответствующих ключам свойства
states
. - Список переходов, определенных для каждого состояния с использованием
EVENT_NAME: “nextState”
пар. - Начальное состояние, поэтому мы знаем, что делать при получении первого события.
Мы можем использовать эту информацию для определения нашего настраиваемого хука:
Как видите, наш настраиваемый Hook - это просто частный случай useReducer
: в соответствии со спецификацией мы используем переходы состояний в свойстве states
для создания функции редуктора, а initialState
в качестве начального состояния редуктора. Возвращаемое значение useReducer
соответствует нашим требованиям API: мы получаем текущее состояние и функцию для отправки (отправки) событий, которые, в свою очередь, обновят состояние машины 😀.
Использование useStateMachine
Теперь мы можем использовать наш собственный Hook для «управления» рендерингом и взаимодействием нашего приложения:
Для каждого возможного состояния отображается отдельный компонент. Каждый компонент получает набор обратных вызовов для отправки событий в конечный автомат и использует эти обратные вызовы для обработки взаимодействия с пользователем.
Тестирование
useStateMachine
- это просто прославленный useReducer
, поэтому его довольно легко проверить. Вы можете распаковать конечный автомат в свой собственный Hook и протестировать его с помощью таких инструментов тестирования, как react-hooks-testing-library. Или вы можете протестировать саму функцию редуктора:
Обслуживание
Самое замечательное в использовании конечного автомата заключается в том, что спецификация кодирует все возможные состояния вашего компонента, а также каждый допустимый переход от состояния к состоянию. Это упрощает добавление новых состояний и переходов.
Давайте изменим наш пример, чтобы учесть возможные ошибки подключения. Теперь наш конечный автомат выглядит так:
Теперь, пока мы находимся в состоянии connecting
, мы можем получить событие CONNECTION_ERROR
и перейти в состояние error
. Находясь в состоянии error
, мы можем попытаться снова CONNECT
.
Мы обновляем наши компоненты для обработки нового состояния:
Конечные автоматы - очень мощный инструмент для разработки компонентов пользовательского интерфейса. Как мы убедились в этой истории, даже простая реализация может быть действительно полезной. Вы можете начать с чего-то вроде этого, а затем, если вам нужно, пойти по пути полномасштабного конечного автомата и добавить защиты перехода, эффекты, вложенные машины и все другие полезности, которые предлагают библиотеки, такие как xstate.