Перейти к содержимому
Поиск
Разработка

XState: состояния интерфейса — мастерская и 9 живых примеров

Девять работающих интерфейсных сценариев XState: запросы и отмена, формы, мастера, согласования, параллельные операции и восстановление черновика. С исходниками и командами для Windows/Linux.

Александр Некрасов
XState — состояния интерфейса, события и переходы

XState на практике: запросы, формы, согласования, таймеры, параллельные операции и сохранение черновика. Девять живых сценариев, понятные правила интерфейса и исходники для Windows и Linux.

Что такое XState и какую задачу он решает

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

Это библиотека JavaScript для машин состояний, statecharts и акторов. Она управляет поведением, а внешний вид остаётся за вашим HTML, React, Vue или другим слоем представления. В этой статье мы используем обычный JavaScript и Bootstrap: девять примеров можно попробовать непосредственно на странице, увидеть текущие данные и историю событий, а затем скачать полный проект.

В проекте закреплён XState 5.33.4. На момент подготовки материала стабильный npm-тег latest относится к этой версии, а XState 6 распространяется как beta. Поэтому примеры используют API v5: setup, createActor, assign, guard и invoke. При чтении документации проверяйте версию: переход на новую основную ветку требует отдельной миграции. Метаданные стабильной версии npm и руководство по API v5.

Где применять: заявки, мастера, запросы и редакторы

Хороший кандидат — процесс, где важен порядок действий. Заявку сначала создают, затем согласуют, иногда возвращают на доработку. В мастере подключения нельзя подтвердить тариф до заполнения профиля. В поиске новый ввод должен отменить прежнее ожидание. Такие правила удобно выразить событиями и состояниями, а не проверять заново в каждом обработчике кнопки.

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

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

Мини-мастерская: девять работающих интерфейсов

Выберите сценарий и сначала нажмите «Показать цепочку действий». Затем начните заново и пройдите собственный путь. В каждом примере видны доступные кнопки, активные состояния, context и история последних событий. В форме можно ошибиться, в запросе — включить отказ, в согласовании — изменить маршрут суммой, в черновике — восстановить сохранённый снимок.

Библиотека XState выполняется по-настоящему в вашей вкладке. Ответы API здесь имитирует локальный Promise с задержкой: мастерская не обращается к рабочему backend, не удаляет документы и не отправляет заполненную форму. Это позволяет безопасно воспроизводить ошибки и отмены. Код локального сервиса открыт, а ниже показано, как заменить его настоящим fetch.

JSON-экспорт сохраняет состояние, данные и историю опыта. В JavaScript-файле находятся все девять машин, поэтому он подключается вместе с XState из проекта. При смене сценария прежний actor останавливается. Снимок черновика держится в памяти вкладки, а после её закрытия исчезает: это осознанная граница учебного примера, а не скрытая серверная синхронизация.

Состояние, событие и actor: минимальный рабочий пример

Машина описывает правила, actor исполняет их. Состояние отвечает на вопрос «что сейчас происходит», событие — «что произошло». Для загрузки это могут быть idle, loading, success и failure, а событиями будут FETCH, CANCEL и RETRY. Событие не обязано менять состояние: оно может обновить данные или оказаться недопустимым в текущей точке процесса.

import {setup, createActor} from 'xstate';

const machine = setup({}).createMachine({
  initial: 'closed',
  states: {
    closed: {on: {OPEN: 'opened'}},
    opened: {on: {CLOSE: 'closed'}}
  }
});
const actor = createActor(machine);
actor.subscribe(snapshot => console.log(snapshot.value));
actor.start();
actor.send({type: 'OPEN'});
actor.stop();

Создание машины само по себе не запускает процесс: нужен actor.start(). Подписка получает snapshots, по которым интерфейс обновляет текст, доступность кнопок и результат. Когда компонент больше не нужен, actor.stop() завершает его работу. Жизненный цикл особенно важен для запросов и таймеров: оставленный без владельца процесс способен продолжать работу после смены экрана. Модель акторов в документации.

Context: данные не нужно превращать в отдельные состояния

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

import {assign} from 'xstate';

const saveName = assign({
  name: ({event}) => String(event.value).slice(0, 80)
});
// Inside a state:
// on: { NAME: { actions: saveName } }

В наших примерах context меняется через assign. Машина согласования хранит сумму, черновик — текст и номер редакции, поиск — запрос и найденные услуги. Обработчик получает текущие context и event, поэтому новое значение можно вычислить из события. Прямое присваивание в объект snapshot.context обходит этот порядок и делает подписки и проверки трудно предсказуемыми. Context и обновление данных.

Пример 1. Загрузка каталога: успех, ошибка и повтор

Представьте карточку услуги в кабинете клиента. До запроса показывается приглашение загрузить данные, во время запроса доступна отмена, после успеха появляется список, а после ошибки — объяснение и повтор. В мастерской это четыре отдельных состояния. Поэтому кнопка RETRY доступна в failure, а CANCEL — только в loading: их поведение определяется текущей моделью.

const actor = createScenario('request');
actor.start();
actor.send({type: 'FETCH', fail: true});
// After the local service rejects: failure.
actor.send({type: 'RETRY', fail: false});
// After the local service resolves: success.

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

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

Invoke и отмена: почему поздний ответ не должен менять экран

Состояние loading запускает Promise actor через invoke. При успехе onDone сохраняет output, при отказе onError выбирает состояние failure. Когда пользователь отменяет загрузку, родитель выходит из loading, а вызванный actor прекращает участвовать в этом процессе. В наших тестах поздний результат после отмены не может переписать состояние idle.

import {fromPromise} from 'xstate';

const loadCatalog = fromPromise(async ({signal, input}) => {
  const response = await fetch('/api/catalog?q=' + encodeURIComponent(input.query), {
    signal,
    headers: {Accept: 'application/json'}
  });
  if (!response.ok) throw new Error('HTTP ' + response.status);
  return response.json();
});

Для фактической отмены сетевой работы передайте signal в fetch. Иначе интерфейс перестанет учитывать ответ, но сама операция может продолжать расходовать ресурсы. При этом даже отменённый HTTP-запрос не доказывает, что сервер успел отменить изменение данных: запрос мог уже завершить запись. Для этого нужны правила серверной операции и, при необходимости, отдельный запрос состояния. Жизненный цикл invoke и Promise actors.

Пример 2. Форма: guard разрешает переход

Форма заявки содержит имя, email и учебное согласие. Событие SUBMIT сначала проверяет guard. При корректных данных машина переходит в accepted; иначе остаётся editing и добавляет понятное сообщение. Попробуйте отправить пустую форму: человек сразу увидит, что именно нужно исправить, а не просто неактивную кнопку без объяснения.

guards: {
  valid: ({context}) =>
    context.name.trim().length >= 2 &&
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(context.email) &&
    context.consent
}

Guard должен отвечать на условие синхронно и без побочных эффектов. Он не место для отправки письма или запроса в базу. Проверку уникальности имени на сервере лучше представить отдельным асинхронным состоянием, после которого будет результат. Тогда пользователь увидит ожидание и сможет отличить неверный ввод от временной недоступности сервиса. Правила guards.

Регулярное выражение в нашей форме — ограниченная учебная проверка формата email, а не доказательство существования адреса. Сервер обязан заново проверить входные данные. Для PHP на сайте есть отдельная статья о Symfony Validator с проверкой полей, JSON и бизнес-правил.

Пример 3. Мастер подключения: шаги и возврат назад

Первичная настройка кабинета состоит из профиля, тарифа, проверки и завершения. У каждого шага свои доступные действия. NEXT из profile требует имя, BACK из review возвращает к тарифу, CONFIRM завершает процесс. Значения сохраняются в context, поэтому при возврате назад пользователь не вводит всё заново.

actor.send({type: 'NAME', value: 'Команда проекта'});
actor.send({type: 'NEXT'});           // profile → plan
actor.send({type: 'PLAN', value: 'solo'});
actor.send({type: 'NEXT'});           // plan → review
actor.send({type: 'BACK'});           // review → plan
actor.send({type: 'NEXT'});
actor.send({type: 'CONFIRM'});        // review → complete

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

Пример 4. Согласование с дополнительным этапом

Заявка на 28 000 после подтверждения руководителя сразу становится approved. Для суммы 75 000 появляется finance: требуется ещё одно подтверждение. Порог 50 000 выбран только для демонстрации маршрута. Он не связан с реальными финансовыми или нормативными требованиями и должен заменяться правилами конкретного приложения.

APPROVE: [
  {guard: 'large', target: 'finance'},
  {target: 'approved'}
]
// In setup: large: ({context}) => context.amount > 50000

Возврат на доработку не стирает заявку. Машина сохраняет причину, а событие EDIT переводит её в draft. Этот маршрут удобен в CRM, согласовании документов и внутренних кабинетах: история показывает, почему объект снова оказался у автора. Важно различать повторное редактирование старой версии и создание нового объекта.

Кнопка «Согласовать этап» в мастерской доступна одному посетителю, чтобы он мог исследовать весь путь. В рабочей системе право действия проверяет сервер по текущему пользователю и актуальному состоянию записи. Клиентский snapshot нельзя считать доказательством полномочий. Если два сотрудника подтверждают одну заявку одновременно, нужна серверная версия записи или другой механизм предотвращения конфликта.

Пример 5. Отложенное удаление и окно отмены

После DELETE карточка скрывается, но машина ещё шесть секунд находится в pending. Событие UNDO возвращает visible. Если время закончилось, after переводит процесс в deleted. Такой интерфейс подходит для черновиков, уведомлений и других операций, где короткое окно отмены помогает исправить случайное нажатие.

pending: {
  after: {grace: 'deleted'},
  on: {UNDO: 'visible'}
}
// setup({delays: {grace: 6000}})

Таймер принадлежит состоянию. После выхода через UNDO он не должен позже удалить возвращённую карточку. В проекте это проверяется управляемыми часами: отмена на 5999-й миллисекунде сохраняет visible после исходного срока. Такой тест полезнее ручного ожидания «примерно шесть секунд», потому что воспроизводит границу условия. Отложенные переходы.

Вкладка браузера не является надёжным планировщиком серверной операции. Она может закрыться или быть приостановлена системой. Для реального удаления храните срок и состояние на сервере, а клиент используйте как представление. Если нужна гарантированная отложенная задача, пригодится отдельная очередь, например уже разобранный на сайте BullMQ.

Пример 6. Параллельная загрузка и проверка

Документ считается готовым, когда завершены две независимые ветви: upload и checks. В мастерской первая заканчивается через 1100 мс, вторая — через 1900 мс. На промежуточном этапе видно, что файл уже готов, а проверка ещё идёт. Общий ready появляется после завершения обеих областей.

preparing: {
  type: 'parallel',
  states: {
    upload: {
      initial: 'running',
      states: {running: {after: {uploadTime: 'done'}}, done: {type: 'final'}}
    },
    checks: {
      initial: 'running',
      states: {running: {after: {checkTime: 'done'}}, done: {type: 'final'}}
    }
  },
  onDone: 'ready'
}

Здесь используется statechart с параллельными областями, а не несколько потоков JavaScript. Обе области логически активны одновременно. Если реальные операции выполняются на backend, их результаты приходят событиями или через invoke. Не стоит показывать сто процентов только по завершению загрузки, когда сервер ещё проверяет формат: разделённое состояние помогает честно отразить оставшуюся работу. Параллельные состояния.

Пример 7. Снимок черновика и восстановление

Измените заголовок и текст, перейдите в предпросмотр и нажмите «Сохранить снимок». Затем начните заново и восстановите снимок. Вернутся и данные, и шаг review. Мы используем getPersistedSnapshot(), сериализуемый снимок внутреннего состояния, а затем передаём его в createActor через параметр snapshot.

const saved = {
  version: 1,
  scenario: 'draft',
  snapshot: actor.getPersistedSnapshot()
};
const restoredActor = createScenario('draft', {
  snapshot: validateDraftCheckpoint(saved)
}).start();

Восстановление в этой мастерской ограничено editing и review. Завершённый published нельзя незаметно импортировать как редактируемый черновик. Проверяется версия оболочки и структура полей, а сам снимок хранится только в памяти вкладки. JSON можно скачать для изучения, но произвольный пользовательский файл мастерская не исполняет и не импортирует.

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

Пример 8. Мгновенное избранное с откатом

Переключатель избранного меняется сразу после клика, а затем ждёт подтверждение. В context отдельно хранятся favorite — текущее отображение и confirmed — последнее подтверждённое значение. При успехе confirmed обновляется. При ошибке favorite возвращается к confirmed, и пользователь получает объяснение.

// Enter saving:
assign({favorite: ({context}) => !context.favorite});

// Successful response:
assign({confirmed: ({context}) => context.favorite});

// Failed response:
assign({favorite: ({context}) => context.confirmed});

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

Не переносите этот шаблон механически на подтверждение оплаты или удаление важных данных. Там надпись об успехе должна соответствовать серверному результату. Даже для избранного полезно различать временную потерю связи и окончательный отказ. После перезагрузки страницы сервер остаётся источником истины, а локальное «сердечко» только показывает текущую обратную связь.

Пример 9. Поиск с debounce и защитой от старых результатов

Событие QUERY переводит машину в debouncing. Если за 450 мс пришёл новый ввод, ожидание начинается заново. После паузы запрос короче двух символов возвращает idle, а подходящий запускает loading. Таким образом, набор слова не создаёт отдельную операцию на каждую букву.

on: {
  QUERY: {
    target: '.debouncing',
    reenter: true,
    actions: assign({query: ({event}) => event.value})
  }
}

Попробуйте набрать «мо», дождаться начала загрузки и изменить запрос на «сеть». Предыдущая операция получит abort, а её поздний ответ не заменит новую выдачу. Это существеннее самого debounce: даже редкие запросы могут завершаться в другом порядке, чем были отправлены. Такой дефект особенно заметен в подсказках и фильтрах при нестабильной сети.

После нового ввода наша мастерская очищает прежний список, чтобы не выдавать его за результат нового запроса. Другой допустимый дизайн — оставить предыдущие данные с явной пометкой обновления. Выбор зависит от продукта, но состояние интерфейса должно объяснять происхождение показанного списка. Пустая выдача, ошибка и отсутствие достаточного ввода также разделены.

Как связать машину с кнопками и полями

Представление подписывается на snapshot и обновляет только видимые элементы. Для проверки состояния удобно matches, а для доступности события — can. В нашей форме SUBMIT остаётся доступным при неверном вводе, потому что у него есть обработчик, показывающий ошибку: can сообщает о возможности обработки события, а не обязательно об успешной бизнес-операции.

actor.subscribe(snapshot => {
  status.textContent = String(snapshot.value);
  cancelButton.disabled = !snapshot.can({type: 'CANCEL'});
  result.hidden = !snapshot.matches('success');
});
cancelButton.addEventListener('click', () => actor.send({type: 'CANCEL'}));

Вводимые значения выводятся через textContent или value, а не превращаются в HTML. Состояние actor — объект приложения, не доверенный шаблон разметки. Также не нужно постоянно пересоздавать активное поле при каждом символе: это сбивает фокус и положение курсора. В мастерской поля создаются при смене сценария, а последующие snapshots обновляют их состояние.

Как подключить настоящий PHP или Node.js API

Машины получают функцию request через createScenario. В учебном варианте это demoRequest; в приложении передайте адаптер fetch. Контракт адаптера должен быть понятным: какие параметры принимает, какой массив возвращает, как сообщает об ошибке. Такой небольшой слой отделяет правила интерфейса от адреса сервера и позволяет тестировать задержки без реального сетевого сервиса.

const request = async ({signal, query = ''}) => {
  const response = await fetch('/api/catalog?q=' + encodeURIComponent(query), {signal});
  if (!response.ok) throw new Error('HTTP ' + response.status);
  const rows = await response.json();
  if (!Array.isArray(rows) || rows.length > 100 || rows.some(row =>
    !Number.isSafeInteger(row.id) || typeof row.name !== 'string' ||
    typeof row.category !== 'string')) throw new Error('Invalid response');
  return rows;
};
const actor = createScenario('search', {request}).start();

Этот фрагмент предполагает существующий endpoint /api/catalog. Он может быть написан на PHP, Fastify или другом сервере. Для запросов записи добавьте подходящую схему авторизации, проверку входных данных и защиту от повторов. Секрет API не должен находиться в клиентском context. Состояние UI описывает ход операции, а сервер принимает окончательное решение о допустимости действия.

Windows: установка и первый запуск из PowerShell

Скачайте архив, распакуйте его в отдельную папку и установите Node.js 24 LTS с официального сайта. В проекте задан минимум 24.15, а зависимости закреплены lock-файлом. Команда npm ci использует зафиксированное дерево; это удобнее для повторения статьи, чем установка произвольных новых версий через npm install без lock.

Set-Location C:\Projects\xstate-project
node --version
npm.cmd ci --ignore-scripts
npm.cmd run build
npm.cmd test
npm.cmd run demo
npm.cmd run serve

Откройте http://127.0.0.1:8181/. Если PowerShell блокирует npm.ps1, используйте npm.cmd, как показано выше; изменение общей политики выполнения не требуется. Сервер завершается Ctrl+C. Команда demo отдельно запускает машину согласования в Node.js и печатает JSON-состояния, поэтому полезна для понимания: XState не зависит от наличия браузерной страницы.

При занятом порте задайте $env:PORT = “8191” перед запуском. В README показано, как убрать эту переменную после остановки. Не открывайте index.html двойным щелчком через file://: ES-модули, получение исходников и другие ресурсы должны загружаться по HTTP. Это обычное ограничение способа запуска, а не ошибка машины состояний.

Linux: запуск проекта и работа по SSH

На Linux используются тот же package.json и тот же lock. Проверьте node –version: системный пакет дистрибутива может содержать другую ветку Node. Устанавливайте подходящий runtime по официальной инструкции, а сам проект держите отдельно от работающего сервиса. Для распаковки архива понадобится unzip.

unzip xstate-project.zip -d "$HOME/projects"
cd "$HOME/projects/xstate-project"
node --version
npm ci --ignore-scripts
npm run build
npm test
npm run demo
npm run serve

Если Node запущен на удалённом VPS, адрес 127.0.0.1 относится к серверу. Чтобы увидеть страницу в браузере своего компьютера, создайте SSH-туннель: ssh -N -L 8181:127.0.0.1:8181 user@your-server. После этого локальный адрес откроет удалённую мастерскую. Этот способ подходит и для штатного OpenSSH в Windows и не требует публичного открытия порта разработки.

Для просмотра уже собранной статики можно использовать существующий локальный web-сервер, включая PHP: php -S 127.0.0.1:8181 -t public. PHP здесь только раздаёт файлы и не выполняет логику XState. Выбор сервера не меняет правила машин: они находятся в JavaScript и выполняются в браузере посетителя.

VPS: размещение готовой мастерской за HTTPS

В production этой мастерской не нужен постоянно работающий Node-процесс. Выполните сборку, затем разместите public как отдельный статический сайт или каталог. Это уменьшает число обслуживаемых процессов. Если приложение уже имеет PHP-backend, можно раздавать готовые файлы через тот же web-сервер и обращаться к API по согласованному относительному пути.

workshop.example.com {
    root * /srv/xstate-workshop/public
    encode zstd gzip
    file_server
}

В примере Caddy нужно заменить домен, настроить DNS и права чтения каталога. Не подменяйте корень действующего сайта учебной папкой. Сначала проверьте отдельный hostname или тестовый маршрут, затем валидируйте конфигурацию и только после этого выполняйте reload. Полные пояснения к HTTPS есть в материале о Caddy.

Важны корректный MIME для JavaScript, сохранность путей assets/vendor/source и доступность ZIP. Не удаляйте старые ресурсы сразу после выкладки новой страницы, если браузеры ещё могут использовать прежний HTML. Для приложения с backend отдельно продумайте совместимость версии клиентской машины и контракта API. Развёртывание статики не проверяет серверные бизнес-правила автоматически.

Что находится в исходниках и как менять пример

В src/machines.js собраны машины, в scenarios.js — подписи и подсказки, в app.js — связь с DOM и экспорт. Каталог tools содержит сборку, локальный HTTP-сервер и консольный опыт. В test находятся проверки переходов и интерфейсных контролов, а public содержит готовую сборку для размещения. Тексты лицензий лежат рядом с исходниками и копируются в публичную часть.

src/machines.js
src/scenarios.js
src/app.js
src/index.html
src/style.css
tools/build.mjs
tools/server.mjs
tools/demo.mjs
test/machines.test.mjs
test/interface.test.mjs
public/

Для первого изменения выберите один сценарий. Например, добавьте в согласование состояние cancelled и разрешите отмену только из draft и manager. Затем напишите ожидаемые переходы и повторите тесты. Менять следует src, после чего запускать build: ручная правка сжатого public/assets/app.js исчезнет при следующей сборке и затруднит сопровождение.

Тестирование: проверяем последовательности, а не только экран

Проект содержит двадцать runtime- и DOM-тестов. Они проверяют реальный XState: успех и отказ запроса, AbortSignal, поздний ответ после отмены, очистку старого списка при новом запросе, guard формы, возврат по шагам, оба маршрута согласования, окно отмены, параллельные ветви, JSON-снимок, optimistic rollback и debounce. Проверка DOM дополнительно связывает состояние машины с доступностью контролов и безопасным выводом пользовательского текста.

import {SimulatedClock} from 'xstate';
import {createScenario} from './src/machines.js';

const clock = new SimulatedClock();
const actor = createScenario('undo', {clock}).start();
actor.send({type: 'DELETE'});
clock.increment(5999);
actor.send({type: 'UNDO'});
clock.increment(100);
console.assert(actor.getSnapshot().value === 'visible');
actor.stop();

Управляемые часы делают проверку границы воспроизводимой. Для запросов тесты подставляют Promise, который можно завершить в нужном порядке, включая неправильный. Это помогает находить ошибки, которые почти не видны при быстром локальном сервере. При этом DOM-тест не равен проверке на физическом телефоне, а учебная сеть не является нагрузочным испытанием вашего API.

Частые ошибки: флаги, мутации и забытые actors

Если в проекте одновременно есть XState и старые isLoading/isSuccess, определите единственный источник состояния экрана. Иначе машина покажет failure, а оставшийся флаг продолжит показывать успех. Обычно отображение лучше получать из snapshot, а самостоятельные переменные оставить только для данных, которые не относятся к процессу.

Если при переключении экрана продолжают появляться результаты, проверьте остановку actor и подписок. Если новые данные не видны, ищите прямую мутацию context вместо assign. Если guard ведёт себя нестабильно, уберите из него сетевую работу, случайность и скрытые изменения: условие перехода должно быть понятным и воспроизводимым для конкретного события.

Ещё одна ошибка — называть любые исключения «неверными данными». Timeout, отказ доступа и пустая выдача требуют разных реакций. Сначала сформулируйте, что может сделать пользователь после каждого исхода: повторить, исправить поле, вернуться назад или обратиться за доступом. Затем отразите эти варианты в модели. Так структура кода начинает помогать интерфейсу, а не усложнять его.

Как внедрить XState в существующий проект постепенно

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

Далее отделите операции от переходов: функцию запроса, проверку полей и сохранение данных оставьте небольшими адаптерами. Подключите actor к существующему экрану и получайте отображение из snapshot. Переносите постепенно, но не оставляйте два независимых механизма, управляющих одной кнопкой. Для сложной схемы можно сначала нарисовать маршрут в Mermaid-мастерской.

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

Частые вопросы: React, Vue, PHP и база данных

Нужен ли React? Нет: эта мастерская работает на обычном JavaScript. У XState есть отдельные интеграции с UI-фреймворками, но правила машин можно держать независимо от компонентов. Это удобно, когда один маршрут используется в нескольких представлениях или проверяется без браузера. Выбирать адаптер следует под конкретную версию вашего фреймворка.

Заменяет ли XState backend или базу? Нет. Он управляет процессом, а хранение документов, права доступа и окончательные проверки остаются у приложения. Node.js может исполнять ту же машину на сервере, но для надёжного рабочего процесса всё равно нужны стратегия сохранения, обработка повторов и согласование с данными. Один JSON snapshot не превращает процесс в отказоустойчивую очередь.

Можно ли использовать вместе с PHP? Да: PHP предоставляет API, а XState управляет ожиданием, ошибками и действиями страницы. В случае простого HTML-приложения сначала сравните сложность задачи с более лёгким вариантом. Если правила ограничены одним запросом и небольшим обновлением фрагмента, уже знакомый htmx может оказаться достаточным; XState особенно полезен там, где важны взаимосвязанные состояния.

Скачать исходники и продолжить изучение

В архиве находятся девять машин, локальные учебные данные, интерфейс мастерской, lock-файл, тесты и README с командами для Windows, Linux и размещения на VPS. Собственный код доступен под MIT; зависимости сохраняют свои лицензии. Можно начать с готового сценария и заменить только адаптер данных, сохранив проверенный маршрут интерфейса.

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