Использование машин
useMachine
Composable. Оборачивает MachineConfig в реактивность Vue и предоставляет богатый API.
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() — очередь событий
События обрабатываются последовательно. Вызов send() несколько раз в один тик ставит все события в очередь и запускает их одно за другим. Каждый send() возвращает Promise, резолвящийся после того, как конкретное событие полностью обработано (включая асинхронные actions).
// Безопасно вызывать быстро подряд — никаких состояний гонки
await send('SUBMIT')
// здесь state равен 'loading'
send('SUCCESS') // поставлен в очередь, не ожидается
send('FAIL') // тоже в очереди — но 'FAIL' будет проигнорирован, так как 'SUCCESS' выполнился первымmatches() — проверка состояния
// Простая строка
matches('loading') // true, если state === 'loading'
// Массив — любое из состояний
matches(['idle', 'error']) // true, если state === 'idle' ИЛИ 'error'
// Объект — проверка параллельного региона
matches({ validation: 'invalid' }) // true, если регион 'validation' в состоянии 'invalid'can() — проверка переходов
can() синхронно вычисляет guard без побочных эффектов. Используйте для включения/отключения кнопок:
const { can } = useMachine(loginForm)
// В шаблоне
// :disabled="!can('RETRY')"Важно: guards, используемые с
can(), должны быть синхронными и свободными от побочных эффектов. Это осознанный контракт —can()вызывается реактивно и не должен запускать асинхронные операции.
Персистентность — снапшот в localStorage
const { state, send } = useMachine(checkoutMachine, {
persist: { key: 'checkout' },
})
// При монтировании: снапшот восстанавливается из localStorage
// При каждом переходе: снапшот сохраняется в localStorageСнапшот включает state, context и history. На сервере (typeof window === 'undefined') персистентность молча отключена.
// Кастомное хранилище
const { send } = useMachine(machine, {
persist: { key: 'my-key', storage: sessionStorage },
})Полный пример — форма входа
<script setup lang="ts">
import { defineMachine, useMachine } 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
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>