Skip to content

State Machine

v0.2.7Состояние и данныеVue

Лёгкие реактивные конечные автоматы (FSM / statechart) для Vue 3 — декларативные состояния и переходы, параллельные регионы, guards и actions.

State Machine
Начать знакомство →
npm install @macrulez/vue-state-machine@latest
01 — Назначение

Когда это пригодится

Кнопка «Сохранить» — это не просто «нажата» или «не нажата», а целая цепочка состояний (загрузка, подтверждение, ошибка, снова доступна) — vue-state-machine описывает эту цепочку одним объявлением, где переходы между состояниями заданы явно и не могут произойти в обход правил.

Кнопка не должна отправлять форму дважды

Пока запрос на сохранение выполняется, повторный клик не должен снова его запускать. Явное состояние «отправляется» делает повторный клик просто невозможным, вместо того чтобы полагаться на отдельную проверку в каждом обработчике.

Многошаговое оформление с условными переходами

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

Два независимых процесса идут одновременно

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

Несколько булевых флагов противоречат друг другу

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

02 — Фичи

Коротко о главном

Декларативные машины состояний и реактивный API

Декларативные машины состояний и реактивный API

defineMachine создаёт чистую конфигурацию с валидацией на этапе разработки — без Vue-зависимостей, тестируемую в Node. useMachine оборачивает её в реактивные Ref: state, context, send, matches, can, isDone. Всё обновляется автоматически при переходе, а очередь событий гарантирует последовательную обработку без гонок.

Guards, действия и обработка событий

Guards, действия и обработка событий

Синхронные guards (предикаты) блокируют переходы и используются в can() для реактивного управления UI. Действия могут быть синхронными или асинхронными, выполняются в порядке exit → transition → entry и возвращают Partial<context> для обновления состояния. Исключения в guards трактуются как false.

Параллельные состояния и useWizard

Параллельные состояния и useWizard

Внутри одного состояния можно запустить параллельные регионы — независимые под-машины, активные одновременно, каждая со своим состоянием и контекстом. useWizard строит машину для многошаговых форм из массива шагов с canProceed (синхронным или асинхронным), колбэками onEnter/onLeave и гибкой навигацией (next, prev, goTo).

Персистентность, шеринг и DevTools

Персистентность, шеринг и DevTools

Сохраняйте состояние и контекст в localStorage (или кастомное Storage) с автоматическим восстановлением. useSharedMachine создаёт синглтон по id, доступный из любых компонентов без Pinia. Vue DevTools-панель показывает все машины, их состояние, контекст и историю переходов, опрашивая дерево компонентов в реальном времени.

Лёгкость, SSR и XState v5 совместимость

Лёгкость, SSR и XState v5 совместимость

Ядро (~4 KB gzip) не содержит браузерных API — безопасно для SSR. persist и DevTools активируются только на клиенте. API совместим с XState v5: defineMachine вместо createMachine, действия возвращают Partial<context> вместо assign, invoke заменяется асинхронными действиями. Полная типизация с выводом TState/TEvent/TContext.

История переходов и снапшоты

История переходов и снапшоты

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

03 — Быстрый пример

Как это работает

Машина с контекстом, guard'ами и actions

Guard блокирует переход, если попыток уже 3, action увеличивает счётчик и сбрасывает ошибку — вся логика формы описана декларативно в одном месте, а не размазана по обработчикам.

machine.ts
import { defineMachine } from 'vue-state-machine'
import type { Action, Guard } from 'vue-state-machine'

type Ctx = { attempts: number; error: string | null }
type Ev = 'SUBMIT' | 'SUCCESS' | 'FAILURE' | 'RETRY'

const resetError: Action<Ctx, Ev> = () => ({ error: null })
const incrementAttempts: Action<Ctx, Ev> = (ctx) => ({ attempts: ctx.attempts + 1 })
const canRetry: Guard<Ctx, Ev> = (ctx) => ctx.attempts < 3

export const loginMachine = defineMachine<'idle' | 'loading' | 'error' | 'success', Ev, Ctx>({
  id: 'login',
  initial: 'idle',
  context: { attempts: 0, error: null },
  states: {
    idle: { on: { SUBMIT: { target: 'loading', actions: [resetError] } } },
    loading: {
      on: {
        SUCCESS: { target: 'success' },
        FAILURE: { target: 'error', actions: [incrementAttempts] },
      },
    },
    error: { on: { RETRY: { target: 'idle', guard: canRetry } } },
    success: { type: 'final' },
  },
})

Подключение в компоненте

send() возвращает промис, дожидающийся конца перехода, can() синхронно проверяет доступность события, isDone включается на финальном состоянии — всё реактивно, без ручных computed.

usage.ts
import { useMachine } from 'vue-state-machine'
import { loginMachine } from './machine'

const { state, context, send, can, isDone } = useMachine(loginMachine)

async function submit() {
  await send('SUBMIT')
  try {
    await api.login()
    send('SUCCESS')
  } catch (e) {
    send({ type: 'FAILURE', message: String(e) })
  }
}

// state.value === 'error'  ->  `Failed. Attempts: ${context.value.attempts}/3`
// can('RETRY')             ->  whether the Retry button should be enabled
// isDone.value             ->  true once login succeeds

Мастер из нескольких шагов — без своей машины состояний

useWizard сам строит машину по массиву шагов: canProceed блокирует next(), пока обязательные поля не заполнены, а progress — готов из коробки.

checkout-wizard.ts
import { useWizard } from 'vue-state-machine'
import type { WizardStep } from 'vue-state-machine'

interface CheckoutCtx {
  name: string
  email: string
  address: string
}

const steps: WizardStep<CheckoutCtx>[] = [
  { id: 'info', label: 'Your info', canProceed: (ctx) => !!ctx.name && !!ctx.email },
  { id: 'address', label: 'Delivery', canProceed: (ctx) => !!ctx.address },
]

const { currentStep, progress, next, prev, isLast } = useWizard(steps)

// next() calls canProceed first and returns false if it's blocked — no
// manual validation gate before advancing to the next step.