Skip to content

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 Node
  • useMachine() — composable that wraps a machine in Vue reactivity; reactive state, 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 queuesend() 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 of useMachine; next(), prev(), goTo(), async canProceed, onEnter/onLeave hooks, circular mode
  • Persist — optional snapshot serialization to localStorage (or any custom Storage) per machine instance
  • Transition history — configurable depth, useful for debugging and undo flows
  • useSharedMachine() — singleton machine shared between unrelated components without Pinia
  • DevTools — separate /devtools entry point; custom panel in Vue DevTools with state, context, history, and event sender
  • Full TypeScriptTState, TEvent, TContext generics inferred automatically from the config
  • XState v5 compatible subset — migrate by swapping createMachinedefineMachine and assign() → plain return value
  • SSR-safe — no window / localStorage in the core; persist is silently skipped server-side
  • ≤ 4 KB gzip for the core (defineMachine + useMachine)

Installation

bash
npm install @macrulez/vue-state-machine

Peer dependency:

bash
npm install vue@>=3.3

Quick 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.