State Management
XState: акторы
Чат-виджет Intercom держит на одной странице десятки независимых процессов одновременно: сокет переподключается, набор сообщения тротлится, загрузка вложения тикает прогрессом, фоновый опрос тянет непрочитанные. Если описать всё это одной плоской машиной, она распухает до сотни состояний и читать её невозможно. XState v5 переворачивает подход: вместо одной гигантской машины - набор мелких акторов, каждый со своим состоянием, которые шлют друг другу события. Машина сокета не знает про индикатор набора, и это правильно.
- Stately.ai (авторы XState) строят редактор машин на акторах: каждая панель и каждое соединение это отдельный спавненный актор
- Чат-виджеты вроде Intercom и Crisp: сокет, тайпинг-индикатор, очередь исходящих сообщений - независимые акторы под одним родителем
- Онбординг-визарды в SaaS: каждый шаг это инвокнутая машина, родитель оркеструет переходы и собирает результат
- Игровые лобби и редакторы (Figma-подобные): десятки живых сущностей, каждая со своим жизненным циклом, общаются сообщениями
- Загрузчики файлов: на каждый файл спавнится актор с прогрессом, ретраями и отменой, не мешая соседям
Предварительные знания
- Машина состояний: состояния, переходы, события и контекст
- Базовый XState v5: createMachine, createActor, setup
- Промисы и async/await на уровне уверенного использования
Как XState пришёл к акторам
Дэвид Куршид выпустил XState в 2017 как реализацию SCXML-машин состояний для JavaScript. Несколько лет основной единицей была одна машина с вложенными (иерархическими и параллельными) состояниями. Но реальные приложения это не одна машина, а множество живых процессов, общающихся между собой - ровно то, что Карл Хьюитт описал актор-моделью ещё в 1973 году. XState v5 (релиз в конце 2023) сделал актор-модель фундаментом: теперь любой запущенный объект это актор, машина это лишь один из видов акторной логики, а invoke и spawn создают дочерних акторов. Каждый актор имеет адрес, почтовый ящик событий и собственное изолированное состояние.
Что такое актор и почему v5 на них построен
Актор это независимая единица вычисления со своим изолированным состоянием и почтовым ящиком (mailbox) входящих событий. Актор не открывает своё состояние наружу и не трогает чужое напрямую. Единственный способ взаимодействия - отправить сообщение. Получив событие, актор может изменить своё состояние, отправить сообщения другим акторам и создать новых акторов. Эту модель сформулировал Карл Хьюитт в 1973 году, и она же лежит в основе Erlang и Akka.
В XState v5 актор - базовая абстракция. Машина состояний это один из видов акторной логики, но не единственный: актором может быть и обёрнутый промис, и observable, и колбэк-логика. createActor запускает корневого актора, а invoke и spawn порождают дочерних. У каждого актора есть адрес (ссылка), через который ему шлют события, и снимок состояния (snapshot), который можно читать, но не мутировать.
- Прямой вызов (общий стейт) — Модули дёргают методы друг друга и читают чужие поля. Связность высокая, гонки появляются там, где два места правят одно состояние.
- Актор-модель (сообщения) — Состояние спрятано внутри актора, наружу только события. Связность низкая, каждый актор обрабатывает свой почтовый ящик последовательно.
Изоляция состояния это и есть ключевое свойство. Раз внешний код не может изменить состояние актора напрямую, единственная точка изменения - обработка события внутри. Это убирает целый класс багов с гонками за общую переменную.
Что отличает актора от обычного объекта с публичными методами?
invoke против spawn
Дочерних акторов в XState создают двумя способами, и разница в их жизненном цикле. invoke привязывает актора к конкретному состоянию: пока состояние активно, актор жив, при выходе из состояния он автоматически останавливается. Это удобно, когда дочерний процесс заранее известен и логически принадлежит одному состоянию - например, запрос к серверу во время состояния loading.
spawn создаёт актора динамически прямо в действии, и такой актор не привязан к одному состоянию - он живёт, пока его явно не остановить или пока родитель жив. spawn нужен, когда число дочерних акторов заранее неизвестно: по одному на каждый загружаемый файл, на каждое сообщение в очереди, на каждое соединение. Ссылки на спавненных акторов обычно складывают в контекст родителя.
| Свойство | invoke | spawn |
|---|---|---|
| Где объявляется | В конфигурации состояния | В действии (assign/raise) |
| Жизненный цикл | Пока активно состояние | Пока не остановлен явно или жив родитель |
| Число акторов | Фиксированное, известно заранее | Динамическое, неизвестно заранее |
| Типичный кейс | Запрос на время loading, дочерняя машина шага | По актору на файл, сообщение, соединение |
Простое правило выбора: если дочерний процесс жёстко привязан к состоянию и его ровно один - invoke. Если процессов несколько и их число меняется в рантайме - spawn со ссылками в контексте.
Менеджер загрузок позволяет добавлять произвольное число файлов, и на каждый нужен свой прогресс и отмена. Что подходит?
Как акторы общаются событиями
Акторы не читают и не пишут состояние друг друга напрямую - они обмениваются событиями. Родитель шлёт сообщение конкретному ребёнку через sendTo, указывая его ссылку или id. Ребёнок отвечает родителю через sendParent. Если родитель инвокнул промис-актора, результат приходит как событие onDone, а ошибка как onError. Так строится дерево акторов, где общение идёт по адресам, а не по общим переменным.
Каждый актор обрабатывает свой почтовый ящик последовательно: события встают в очередь и применяются по одному. Поэтому внутри одного актора гонок за его собственное состояние не бывает. Параллелизм возникает между акторами, и координируется он сообщениями, а не блокировками. Это та же дисциплина, что в Erlang: тысячи процессов, каждый со своим состоянием, общение только сообщениями.
Соблазн обойти модель: дотянуться до снимка дочернего актора и прочитать его поля в обход событий. Читать snapshot допустимо, но менять состояние ребёнка можно только событием через sendTo. Прямая мутация чужого контекста ломает изоляцию и возвращает гонки.
Дочерний актор завершил работу и должен передать результат родителю. Как это делается в актор-модели XState?
Связь с другими темами
Урок про акторов как способ декомпозиции. Дальше тема раскрывается так:
- XState: основы — Машина из базового урока становится логикой одного актора. Акторы это следующий слой над ней
- Машины: когда уместны — Акторы мощны, но не бесплатны. Следующий урок про границу, за которой машина оправдана
Итог
- Актор-модель: независимая единица со своим изолированным состоянием и почтовым ящиком событий, общается только сообщениями. XState v5 построен на ней целиком
- invoke привязывает дочернего актора к состоянию: актор живёт, пока активно состояние, и автоматически останавливается при выходе из него
- spawn создаёт актора динамически из действия, когда число дочерних сущностей заранее неизвестно (по актору на файл, на сообщение, на соединение)
- Акторы не лезут в чужое состояние напрямую: родитель шлёт событие через sendTo, ребёнок отвечает через sendParent или sendBack
- Декомпозиция на акторов держит каждую машину маленькой и читаемой вместо одной плоской машины на сотню состояний
Связанные уроки
- sm-26-xstate-basics — Акторы наслаиваются на машину состояний из базового урока: без понимания состояний и переходов разговор про spawn не складывается
- sm-29-xstate-when — Поняв акторов, легче решать, где машина оправдана, а где это перебор