Reference
TypeScript types
All public types are exported from the package root:
ts
import type {
// Core config
MachineConfig,
StateConfig,
TransitionConfig,
SubMachineConfig,
// Functions
Guard,
Action,
// Events
EventObject,
// Runtime
MachineInstance,
UseMachineOptions,
TransitionRecord,
MachineSnapshot,
TransitionResult,
// Wizard
WizardStep,
WizardOptions,
WizardInstance,
// Store
MachineStoreAPI,
// Utility
Ctx,
} from 'vue-state-machine'Generic inference
TypeScript infers TState, TEvent, and TContext from the config you pass to defineMachine. You rarely need to annotate them explicitly:
ts
const machine = defineMachine({
id: 'traffic',
initial: 'red', // TS infers TState = 'red' | 'green' | 'yellow'
states: {
red: { on: { NEXT: { target: 'green' } } }, // TEvent = 'NEXT'
green: { on: { NEXT: { target: 'yellow' } } },
yellow: { on: { NEXT: { target: 'red' } } },
},
})
const { state } = useMachine(machine)
// state: Ref<'red' | 'green' | 'yellow'>
// send accepts only 'NEXT' — other strings are compile errorsFor complex cases you can annotate explicitly:
ts
const machine = defineMachine<
'idle' | 'loading' | 'error' | 'success',
'SUBMIT' | 'SUCCESS' | 'FAILURE' | 'RETRY',
{ attempts: number; error: string | null }
>({ ... })SSR compatibility
| Scenario | Behaviour |
|---|---|
| Server render | Core modules (defineMachine, MachineRunner, useMachine) have no window / document / localStorage references |
persist on server | Silently disabled — typeof window === 'undefined' guard in the composable |
| Hydration | Call restore(serverSnapshot) inside onMounted to hydrate from a server-side snapshot without re-running guards or actions |
snapshot | Serializable with JSON.stringify — pass from server to client via Nuxt useState, useServerState, or <script> injection |
Nuxt SSR example:
vue
<script setup lang="ts">
import { useMachine } from 'vue-state-machine'
import { onMounted } from 'vue'
// Snapshot passed from the server via useAsyncData / useState
const serverSnapshot = useState('checkout-snapshot')
const { state, send, restore } = useMachine(checkoutMachine)
onMounted(() => {
if (serverSnapshot.value) restore(serverSnapshot.value)
})
</script>Architecture
defineMachine(config)
│
▼ dev-time validation + type narrowing
MachineConfig<TState, TEvent, TContext>
│
▼ created inside useMachine()
MachineRunner (pure class, zero Vue deps)
│ getCurrentState() / getContext()
│ canTransition(event) → boolean
│ enqueue(event) ──────────────────────────────┐
│ transition(event) → Promise<TransitionResult> │
│ │
│ EventQueue (sequential processing) │
│ ├── guard check (sync, exception = false) │
│ ├── exit actions (await each) │
│ ├── transition actions (await each) │
│ ├── state update │
│ └── entry actions (await each) │
│ └── Partial<TContext> merged into context ◄┘
│
│ Parallel regions
│ ├── SubMachineRunner per region (activated on state entry)
│ ├── send() dispatches to all regions
│ └── "last declared wins" on context conflict
│
▼ wrapped in Vue reactivity
useMachine(config, options)
│ state: shallowRef<TState>
│ context: shallowRef<TContext>
│ history: shallowRef<TransitionRecord[]> (FIFO, historyLimit)
│ send() → enqueue → sync refs after result
│ matches() / can()
│ snapshot / restore()
│ onMounted: load persist snapshot
│ on transition: save persist snapshot
│
├──▶ MachineStore (provide/inject via VueMachinePlugin)
│ register() on composable creation
│ useSharedMachine() → singleton by config.id
│
▼
Vue components (template, setup)
useWizard(steps, options)
│ buildWizardMachine() → generates MachineConfig from steps array
│ useMachine(generatedConfig)
│ next() → await canProceed → send('NEXT')
│ goTo(id) → await canProceed (if forward) → send('GOTO_<id>')
│ prev() → send('PREV')
│
▼
WizardInstance (currentStep, progress, isFirst, isLast, history, ...)
VueMachineDevtools (separate entry point /devtools)
│ reads MachineStore via app._context.provides
│ hooks into __VUE_DEVTOOLS_GLOBAL_HOOK__
│ emits timeline events per transition
▼
Vue DevTools browser extension panel "State Machines"Bundle size & peer dependencies
| Entry point | Peer deps | Gzip |
|---|---|---|
vue-state-machine | vue ^3.3 | ≤ 4 KB (core) |
vue-state-machine/devtools | vue ^3.3, @vue/devtools-api (peer) | separate chunk |
- Ships as tree-shakeable ESM (
dist/index.mjs) and CommonJS (dist/index.cjs) "sideEffects": falseinpackage.json— bundlers can eliminate unused exports- The
/devtoolsentry point is a separate chunk — importing it inif (import.meta.env.DEV)blocks ensures it is excluded from production bundles by standard tree-shaking
License
MIT