vue-state-machine
Lightweight reactive finite state machines (FSM / statechart) for Vue 3 — declarative states and transitions, parallel regions, guards, actions, persist, and a composable API — with a single peer dependency.
Features
defineMachine()— pure config factory with dev-time validation; no Vue dependency — testable in NodeuseMachine()— composable that wraps a machine in Vue reactivity; reactivestate,context,send(),matches(),can()- Guards — synchronous predicates that block transitions; exception treated as
false - Actions — sync or async side-effects on entry, exit, or transition; return
Partial<context>to update state - Event queue —
send()adds to a queue and processes events sequentially; no race conditions with async actions - Parallel regions — multiple independent sub-machines active at the same time inside a state
useWizard()— built on top ofuseMachine;next(),prev(),goTo(), asynccanProceed,onEnter/onLeavehooks, circular mode- Persist — optional snapshot serialization to
localStorage(or any customStorage) per machine instance - Transition history — configurable depth, useful for debugging and undo flows
useSharedMachine()— singleton machine shared between unrelated components without Pinia- DevTools — separate
/devtoolsentry point; custom panel in Vue DevTools with state, context, history, and event sender - Full TypeScript —
TState,TEvent,TContextgenerics inferred automatically from the config - XState v5 compatible subset — migrate by swapping
createMachine→defineMachineandassign()→ plain return value - SSR-safe — no
window/localStoragein the core; persist is silently skipped server-side - ≤ 4 KB gzip for the core (
defineMachine+useMachine)
Installation
bash
npm install @macrulez/vue-state-machinePeer dependency:
bash
npm install vue@>=3.3Quick start
vue
<script setup lang="ts">
import { defineMachine, useMachine } from 'vue-state-machine'
const trafficLight = defineMachine({
id: 'traffic',
initial: 'red',
states: {
red: { on: { NEXT: { target: 'green' } } },
green: { on: { NEXT: { target: 'yellow' } } },
yellow: { on: { NEXT: { target: 'red' } } },
},
})
const { state, send } = useMachine(trafficLight)
</script>
<template>
<div :class="state">
<p>Current: {{ state }}</p>
<button @click="send('NEXT')">Next</button>
</div>
</template>state is a reactive Ref<'red' | 'green' | 'yellow'>. Clicking the button transitions the machine and Vue re-renders automatically.