Skip to content

Reference ​

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 ◄┘
    │       (contextPatch on TransitionResult = union of all
    │        partials returned this transition, distinct from
    │        nextContext = the full resulting context)
    │
    │  Parallel regions
    │  ├── SubMachineRunner per region (activated on state entry)
    │  ├── context deep-cloned (structuredClone) per instance/region
    │  ├── send() dispatches to all regions
    │  └── "last declared wins" on context conflict — only the fields
    │        a region's own actions touched are ever merged/compared
    │
    ▼ wrapped in Vue reactivity
useMachine(config, options)
    │  state:   shallowRef<TState>
    │  context: shallowRef<TContext>
    │  history: shallowRef<TransitionRecord[]>  (FIFO, historyLimit)
    │  send()   → enqueue → sync refs after result (including a
    │             region-only context change, via TransitionResult.changed)
    │  matches() / can()
    │  snapshot / restore()
    │  onMounted: load persist snapshot
    │  on transition: save persist snapshot
    │
    ├──▶ MachineStore (provide/inject via VueMachinePlugin)
    │        register()/retain() on composable creation, reference-counted
    │        onUnmounted: unregister() (removed once refcount hits 0)
    │        useSharedMachine() → singleton by config.id, retain()s on reuse
    │
    ▼
Vue components (template, setup)

useWizard(steps, options)
    │  buildWizardMachine() → generates MachineConfig from steps array,
    │    with a unique id per instance (options.id, or auto-generated)
    │  useMachine(generatedConfig)
    │  next() → await canProceed(context.value) → send('NEXT')
    │  goTo(id) → await canProceed (if forward) → send('GOTO_<id>')
    │  prev() → send('PREV')
    │  onEnter/onLeave may return Partial<TContext> → merged into the
    │    same live context canProceed reads
    │
    ▼
WizardInstance (currentStep, progress, isFirst, isLast, history, context, ...)

VueMachineDevtools (separate entry point /devtools)
    │  reads MachineStore via app._context.provides
    │  hooks into __VUE_DEVTOOLS_GLOBAL_HOOK__
    │  emits one timeline event per machine on each visitComponentTree poll
    │  (a live snapshot per inspection, not a push per send())
    ▼
Vue DevTools browser extension panel "State Machines"

SSR compatibility ​

ScenarioBehaviour
Server renderCore modules (defineMachine, MachineRunner, useMachine) have no window / document / localStorage references
persist on serverSilently disabled — typeof window === 'undefined' guard in the composable
HydrationCall restore(serverSnapshot) inside onMounted to hydrate from a server-side snapshot without re-running guards or actions
snapshotSerializable 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 '@macrulez/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>

Bundle size & peer dependencies ​

Entry pointPeer depsGzip
@macrulez/vue-state-machinevue ^3.3≤ 4 KB (core)
@macrulez/vue-state-machine/devtoolsvue ^3.3separate chunk
  • Ships as tree-shakeable ESM (dist/index.mjs) and CommonJS (dist/index.cjs)
  • "sideEffects": false in package.json — bundlers can eliminate unused exports
  • The /devtools entry point is a separate chunk — importing it in if (import.meta.env.DEV) blocks ensures it is excluded from production bundles by standard tree-shaking

Development ​

bash
git clone https://github.com/macrulezru/vue-state-machine.git
cd vue-state-machine
npm install

npm run build       # vite build
npm test            # vitest run
npm run typecheck   # tsc --noEmit
npm run lint        # eslint --fix

License ​

MIT