В своем посте Прекратите использовать логические значения isLoading Кент С. Доддс говорит нам… ну, прекратить использование логических значений для отслеживания двоичных состояний 😅. Проблема с использованием логического флага для моделирования состояния нашего компонента заключается в том, что обычно флаг является лишь частью более крупного и сложного состояния.

Когда вы кодируете состояние вашего компонента с помощью набора двоичных флагов, вы заканчиваете рендеринг с большим количеством условных проверок. Это приводит к коду, который действительно сложно поддерживать и расширять.

Вместо использования логических значений Кент рекомендует использовать конечный автомат. В своем посте он использует полноценную библиотеку конечных автоматов xstate.

В этой истории я предложу более простую альтернативу, реализованную в виде Custom Hook с использованием useReducer.

Конечно, поскольку эта альтернатива проще, она также менее эффективна, поэтому, если вы создаете сложный компонент, попробуйте xstate!

Определение useStateMachine

Цитируя Википедию, конечный автомат (или конечный автомат, FSM) - это

[…] Абстрактная машина, которая может находиться ровно в одном из конечного числа состояний в любой момент времени. Автомат может переходить из одного состояния в другое в ответ на некоторые входные данные; переход из одного состояния в другое называется переходом. Автомат определяется списком его состояний, его начальным состоянием и входами, которые запускают каждый переход.

Итак, чтобы построить собственный конечный автомат, нам понадобятся:

  1. Список состояний.
  2. Список событий (входов).
  3. Список переходов (текущее состояние + событие = ›следующее состояние. Этот выглядит знакомым…).

Мы собираемся начать разработку API для нашего useStateMachine пользовательского хука. Нам нужно что-то вроде этого:

const [currentState, sendEvent] = useStateMachine(machineSpec);

Hook принимает объект спецификации машины, определяющий состояния, входы и переходы, и возвращает текущее состояние и метод для отправки события на машину, чтобы заставить ее измениться (перейти) в новое состояние.

Скажем, мы хотим использовать конечный автомат для обработки подключения к некоторой удаленной службе. Наш пользователь начинает отключаться, и мы хотим, чтобы он подключился к услуге и отключился от нее. Мы можем представить эту машину так:

Спецификация кодирует всю информацию, необходимую для запуска машины:

  1. Список состояний, соответствующих ключам свойства states.
  2. Список переходов, определенных для каждого состояния с использованием EVENT_NAME: “nextState” пар.
  3. Начальное состояние, поэтому мы знаем, что делать при получении первого события.

Мы можем использовать эту информацию для определения нашего настраиваемого хука:

Как видите, наш настраиваемый Hook - это просто частный случай useReducer: в соответствии со спецификацией мы используем переходы состояний в свойстве states для создания функции редуктора, а initialState в качестве начального состояния редуктора. Возвращаемое значение useReducer соответствует нашим требованиям API: мы получаем текущее состояние и функцию для отправки (отправки) событий, которые, в свою очередь, обновят состояние машины 😀.

Использование useStateMachine

Теперь мы можем использовать наш собственный Hook для «управления» рендерингом и взаимодействием нашего приложения:

Для каждого возможного состояния отображается отдельный компонент. Каждый компонент получает набор обратных вызовов для отправки событий в конечный автомат и использует эти обратные вызовы для обработки взаимодействия с пользователем.

Тестирование

useStateMachine - это просто прославленный useReducer, поэтому его довольно легко проверить. Вы можете распаковать конечный автомат в свой собственный Hook и протестировать его с помощью таких инструментов тестирования, как react-hooks-testing-library. Или вы можете протестировать саму функцию редуктора:

Обслуживание

Самое замечательное в использовании конечного автомата заключается в том, что спецификация кодирует все возможные состояния вашего компонента, а также каждый допустимый переход от состояния к состоянию. Это упрощает добавление новых состояний и переходов.

Давайте изменим наш пример, чтобы учесть возможные ошибки подключения. Теперь наш конечный автомат выглядит так:

Теперь, пока мы находимся в состоянии connecting, мы можем получить событие CONNECTION_ERROR и перейти в состояние error. Находясь в состоянии error, мы можем попытаться снова CONNECT.

Мы обновляем наши компоненты для обработки нового состояния:

Конечные автоматы - очень мощный инструмент для разработки компонентов пользовательского интерфейса. Как мы убедились в этой истории, даже простая реализация может быть действительно полезной. Вы можете начать с чего-то вроде этого, а затем, если вам нужно, пойти по пути полномасштабного конечного автомата и добавить защиты перехода, эффекты, вложенные машины и все другие полезности, которые предлагают библиотеки, такие как xstate.