XState v5 Migration
vue-state-machine is API-compatible with a useful subset of XState v5. Migrating a simple machine typically takes minutes.
API mapping
| XState v5 | vue-state-machine | Notes |
|---|---|---|
createMachine(config) | defineMachine(config) | Config structure is identical |
useMachine(machine) from @xstate/vue | useMachine(config) | Same composable shape |
send(event) | send(event) | Identical |
matches(state) | matches(state) | Identical |
context in config | context in config | Identical |
on handlers | on handlers | Identical |
entry / exit | entry / exit | Identical |
type: 'final' | type: 'final' | Identical |
guard function | guard function | Same signature |
assign(updater) | Return Partial<context> from action | No wrapper needed |
snapshot / restore | snapshot / restore | Identical concept |
invoke / services | Not supported | Move async work into actions |
spawn / actor model | Not supported | Intentional scope limit |
| Hierarchical states | Not supported | Flat + parallel only |
Step-by-step migration
1. Replace the import and factory:
ts
// Before (XState v5)
import { createMachine } from 'xstate'
const machine = createMachine({ ... })
// After
import { defineMachine } from 'vue-state-machine'
const machine = defineMachine({ ... })2. Replace assign() with plain return values:
ts
// Before
import { assign } from 'xstate'
const increment = assign({ count: (ctx) => ctx.count + 1 })
// After — just return a partial context object
const increment = (ctx: { count: number }) => ({ count: ctx.count + 1 })3. Replace the Vue composable import:
ts
// Before
import { useMachine } from '@xstate/vue'
// After
import { useMachine } from 'vue-state-machine'4. Move async logic from invoke into actions:
ts
// Before (XState v5 invoke)
loading: {
invoke: {
src: (ctx, event) => fetch('/api/user'),
onDone: { target: 'success', actions: assign({ user: (_, e) => e.data }) },
onError: { target: 'error' },
}
}
// After — fire-and-forget inside the component or inside entry action
loading: {
entry: [async (ctx, event) => {
try {
const user = await fetch('/api/user').then(r => r.json())
return { user } // merged into context; then send SUCCESS externally
} catch {
return { error: 'Failed' }
}
}]
}