State Management

Машины состояний: зачем

Форма загрузки данных обрастает флагами: isLoading, isError, isSuccess, плюс data и error. Четыре булевых поля дают шестнадцать комбинаций, но осмысленных среди них четыре. Что значит isLoading вместе с isSuccess? А isError вместе с isSuccess? Эти состояния невозможны по смыслу, но возможны в коде, и ровно из них рождаются баги вроде спиннера поверх данных. Машина состояний задаёт вопрос: а что, если состояний ровно четыре и переход между ними разрешён только по явным правилам.

  • Загрузка данных: переходы idle, loading, success, error вместо набора булевых флагов
  • Формы и мастера: шаги и условия перехода между ними как явные состояния
  • Платежи и заказы: статусы и разрешённые переходы, где случайный скачок недопустим
  • Плееры и медиа: playing, paused, buffering, ended как взаимоисключающие состояния
  • Авторизация: anonymous, authenticating, authenticated, с чёткими переходами между ними

Предварительные знания

  • Таксономия состояния: разные виды состояния и их природа
  • Идея взаимоисключающих состояний, которые не могут быть истинными одновременно
  • Базовое понимание, что булевы флаги перемножают число комбинаций
  • Таксономия состояния

Откуда взялись конечные автоматы

Конечный автомат это одна из старейших моделей вычислений. Уоррен Маккалок и Уолтер Питтс описали машины с конечным числом состояний ещё в 1943 году в работе о нейронной активности, а в 1950-х Мур и Мили формализовали автоматы, носящие их имена. Идея проста и сильна: система всегда находится ровно в одном из конечного набора состояний, а переходы между ними происходят только в ответ на заданные события и только по разрешённым правилам. Десятилетиями автоматы жили в железе, парсерах и протоколах, а в 2010-х вернулись в UI как способ обуздать комбинаторный взрыв булевых флагов.

Суп из булевых флагов и комбинаторный взрыв

Типичный способ описать загрузку данных это набор булевых флагов. isLoading показывает спиннер, isError показывает ошибку, isSuccess показывает данные. Проблема в том, что флаги независимы: четыре булевых поля дают шестнадцать комбинаций, но осмысленных среди них всего четыре. Остальные двенадцать это невозможные по смыслу состояния, которые код всё равно позволяет выразить.

Объект broken проходит проверку типов, но не имеет смысла: загрузка и ошибка одновременно. Именно из таких комбинаций рождаются баги. Компонент проверяет isLoading и рисует спиннер, потом проверяет isSuccess и рисует данные, и при рассогласованных флагах спиннер оказывается поверх данных. Логика расползается по проверкам, и каждая новая проверка добавляет шанс на несогласованность.

isLoadingisErrorisSuccessСмысл
truefalsefalseИдёт загрузка
falsetruefalseПроизошла ошибка
falsefalsetrueДанные получены
truetruefalseБессмыслица: грузим и ошибка
truefalsetrueБессмыслица: грузим и успех

Число флагов растёт линейно, а число комбинаций экспоненциально: N независимых булевых полей это два в степени N состояний. Пять флагов это уже тридцать две комбинации, из которых осмысленны единицы. Отслеживать вручную, какие комбинации валидны, становится невозможно, и баги невозможных состояний накапливаются.

Почему набор из независимых флагов isLoading, isError, isSuccess это источник багов?

Одно состояние из конечного набора

Конечный автомат переворачивает модель. Вместо нескольких независимых флагов есть одно поле состояния, которое принимает значение из конечного списка: idle, loading, success, error. Система всегда ровно в одном из них. Комбинация загрузки и ошибки просто невыразима: поле не может одновременно равняться loading и error.

Это и есть идея сделать невозможные состояния невозможными. Размеченное объединение привязывает данные к состоянию: data существует только в success, error только в error. В состоянии loading нет ни data, ни error, и обратиться к ним нельзя. Компилятор требует обработать все случаи, поэтому забыть состояние не выйдет, а спиннер поверх данных становится невыразимым.

  • Независимые флаги — Шестнадцать комбинаций на четыре булевых поля, осмысленны четыре. Невозможные состояния выразимы и порождают баги
  • Одно поле состояния — Ровно четыре значения, каждое осмысленно. Данные привязаны к состоянию, невозможные комбинации невыразимы

Размеченное объединение в TypeScript это уже шаг к машине состояний: оно гарантирует, что в каждый момент состояние одно и данные согласованы с ним. Машина добавляет к этому второй элемент: правила, какие переходы между состояниями вообще разрешены.

Как одно поле status со значениями idle, loading, success, error решает проблему супа из флагов?

Явные переходы вместо разбросанных проверок

Второй элемент машины состояний это переходы. Мало иметь конечный набор состояний: важно задать, из какого состояния по какому событию в какое можно перейти. Из idle по событию FETCH переходим в loading. Из loading по RESOLVE в success, по REJECT в error. Из error по RETRY снова в loading. Переход, не описанный в схеме, не происходит: случайный скачок из idle в success невозможен.

Главная выгода в том, что вся логика переходов собрана в одном месте, а не разбросана по компонентам в виде разрозненных проверок флагов. Схема отвечает на вопрос что произойдёт по событию X в состоянии Y однозначно. Невозможные переходы исключаются так же, как невозможные состояния: их просто нет в описании машины.

СостояниеСобытиеНовое состояние
idleFETCHloading
loadingRESOLVEsuccess
loadingREJECTerror
errorRETRYloading
successFETCHloading

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

Что добавляют явные переходы поверх конечного набора состояний?

Связь с другими темами

Этот урок открывает группу машин состояний. Дальше идёт реализация:

  • Таксономия состояния — Помогает увидеть, какие куски состояния естественно ложатся на конечный автомат
  • XState: основы — Конкретная реализация конечных автоматов в JavaScript через createMachine

Итог

  • Конечный автомат это конечный набор состояний, где система всегда ровно в одном из них
  • Переходы происходят только по событиям и только по разрешённым правилам, а не произвольно
  • Несколько булевых флагов дают комбинаторный взрыв: N флагов это два в степени N комбинаций
  • Большинство комбинаций флагов бессмысленны, и из них рождаются баги вроде спиннера поверх данных
  • Машина состояний делает невозможные состояния невозможными: бессмысленную комбинацию выразить нельзя
  • Явные переходы заменяют разбросанные по коду проверки флагов одной понятной схемой

Связанные уроки

  • sm-02-state-taxonomy — Таксономия состояния помогает увидеть, какие куски естественно ложатся на конечный автомат
  • sm-26-xstate-basics — Поняв зачем нужны машины состояний, дальше реализуем их в XState
Машины состояний: зачем

0

1

Войти