Skip to content

Using Machines

useMachine

Composable. Wraps a MachineConfig in Vue reactivity and exposes a rich API.

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

Options

OptionTypeDefaultDescription
historyLimitnumber50Maximum entries kept in history; oldest are dropped when exceeded (FIFO)
persist.keystringlocalStorage key for snapshot persistence
persist.storageStoragelocalStorageCustom storage backend (e.g. sessionStorage)

Return value

PropertyTypeDescription
stateReadonly<Ref<TState>>Current state — reactive
contextReadonly<Ref<TContext>>Current context — reactive
send(event: TEvent | EventObject<TEvent>) => Promise<void>Queue an event; resolves after the transition completes
matches(query) => booleanCheck current state or region state (see below)
can(event: TEvent) => booleantrue if the event would trigger a transition (guard evaluated synchronously)
historyReadonly<Ref<TransitionRecord[]>>Past transitions, newest last
isDoneComputedRef<boolean>true when the current state has type: 'final'
snapshotComputedRef<MachineSnapshot>Serializable snapshot of { state, context, history }
restore(snapshot: MachineSnapshot) => voidRestore state from a snapshot without running guards or actions

send() — event queue

Events are processed sequentially. Calling send() multiple times in the same tick queues all events and runs them one after the other. Each send() returns a Promise that resolves after that specific event is fully processed (including async actions).

ts
// Safe to call in rapid succession — no race conditions
await send('SUBMIT')
// state is 'loading' here

send('SUCCESS') // queued, not awaited
send('FAIL') // also queued — but 'FAIL' will be ignored because 'SUCCESS' ran first

matches() — checking state

ts
// Simple string
matches('loading') // true if state === 'loading'

// Array — any of the states
matches(['idle', 'error']) // true if state === 'idle' OR 'error'

// Object — check a parallel region
matches({ validation: 'invalid' }) // true if region 'validation' is in 'invalid'

can() — checking transitions

can() evaluates the guard synchronously without side-effects. Use it to enable/disable buttons:

ts
const { can } = useMachine(loginForm)

// In template
// :disabled="!can('RETRY')"

Important: Guards used with can() must be synchronous and free of side-effects. This is a deliberate contract — can() is called reactively and must not trigger async operations.

Persist — snapshot to localStorage

ts
const { state, send } = useMachine(checkoutMachine, {
  persist: { key: 'checkout' },
})
// On mount: snapshot is restored from localStorage
// On every transition: snapshot is saved to localStorage

The snapshot includes state, context, and history. On the server (typeof window === 'undefined') persist is silently disabled.

ts
// Custom storage
const { send } = useMachine(machine, {
  persist: { key: 'my-key', storage: sessionStorage },
})

Full example — login form

vue
<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>