Skip to content

Использование машин ​

useMachine(config, options?) — composable, оборачивающий MachineConfig в реактивность Vue и предоставляющий богатый API.

ts
function useMachine<TState, TEvent, TContext>(
  config: MachineConfig<TState, TEvent, TContext>,
  options?: UseMachineOptions,
): MachineInstance<TState, TEvent, TContext>

Опции ​

historyLimit ​

number · по умолчанию: 50. Максимум записей в history; старые отбрасываются при превышении (FIFO).

persist.key ​

string, опционально. Ключ localStorage для персистентности снапшота.

persist.storage ​

Storage · по умолчанию: localStorage. Кастомный бэкенд хранилища (например, sessionStorage).

Возвращаемое значение ​

state ​

Readonly<Ref<TState>>. Текущее состояние — реактивное.

context ​

Readonly<Ref<TContext>>. Текущий контекст — реактивный.

send ​

(event: TEvent | EventObject<TEvent>) => Promise<void>. Ставит событие в очередь; резолвится после завершения перехода.

matches ​

(query) => boolean. Проверка текущего состояния или состояния региона (см. ниже).

can ​

(event: TEvent) => boolean. true, если событие вызвало бы переход (guard вычисляется синхронно).

history ​

Readonly<Ref<TransitionRecord[]>>. Прошлые переходы, самый новый — последний.

isDone ​

ComputedRef<boolean>. true, когда текущее состояние имеет type: 'final'.

snapshot ​

ComputedRef<MachineSnapshot>. Сериализуемый снапшот { state, context, history }.

restore ​

(snapshot: MachineSnapshot) => void. Восстанавливает состояние из снапшота без запуска guards или actions.

Отправка событий ​

send(event) — события обрабатываются последовательно. Вызов send() несколько раз в один тик ставит все события в очередь и запускает их одно за другим. Каждый send() возвращает Promise, резолвящийся после того, как конкретное событие полностью обработано (включая асинхронные actions).

ts
// Безопасно вызывать быстро подряд — никаких состояний гонки
await send('SUBMIT')
// здесь state равен 'loading'

send('SUCCESS') // поставлен в очередь, не ожидается
send('FAIL') // тоже в очереди — но 'FAIL' будет проигнорирован, так как 'SUCCESS' выполнился первым

Проверка текущего состояния ​

matches(query):

ts
// Простая строка
matches('loading') // true, если state === 'loading'

// Массив — любое из состояний
matches(['idle', 'error']) // true, если state === 'idle' ИЛИ 'error'

// Объект — проверка параллельного региона
matches({ validation: 'invalid' }) // true, если регион 'validation' в состоянии 'invalid'

Проверка возможности перехода ​

can(event) синхронно вычисляет guard без побочных эффектов. Используйте для включения/отключения кнопок:

ts
const { can } = useMachine(loginForm)

// В шаблоне
// :disabled="!can('RETRY')"

Важно: guards, используемые с can(), должны быть синхронными и свободными от побочных эффектов. Это осознанный контракт — can() вызывается реактивно и не должен запускать асинхронные операции.

Персистентность в хранилище ​

ts
const { state, send } = useMachine(checkoutMachine, {
  persist: { key: 'checkout' },
})
// При монтировании: снапшот восстанавливается из localStorage
// При каждом переходе: снапшот сохраняется в localStorage

Снапшот включает state, context и history. На сервере (typeof window === 'undefined') персистентность молча отключена.

ts
// Кастомное хранилище
const { send } = useMachine(machine, {
  persist: { key: 'my-key', storage: sessionStorage },
})

Полный пример — форма входа ​

vue
<script setup lang="ts">
import { defineMachine, useMachine } from '@macrulez/vue-state-machine'
import type { Action, Guard } from '@macrulez/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

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' },
  },
})

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) })
  }
}
</script>

<template>
  <form @submit.prevent="submit">
    <p v-if="state === 'error'">Failed. Attempts: {{ context.attempts }}/3</p>
    <button type="submit" :disabled="state === 'loading'">Login</button>
    <button v-if="state === 'error'" @click="send('RETRY')" :disabled="!can('RETRY')">Retry</button>
    <p v-if="isDone">Logged in!</p>
  </form>
</template>