npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@macrulez/vue-state-machine

v0.2.6

Published

Reactive finite state machines and statecharts for Vue 3 — declarative transitions, parallel regions, guards, actions and persistence.

Readme

State Machine

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 that can write to context, 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 showing every registered machine's state and context
  • 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)

When you'd reach for this

A "Save" button isn't just "clicked" or "not clicked" — it's a whole chain of states (loading, confirming, error, available again), and vue-state-machine describes that chain as a single declaration where transitions are explicit and can't happen outside the rules.

  • A button shouldn't submit twice — While a save request is still running, clicking again shouldn't fire it a second time. Making "submitting" an explicit state makes a second click simply impossible, instead of relying on a separate check in every handler.
  • A multi-step checkout with conditional branches — A checkout step might require payment for some users and skip it for others, and going back isn't always allowed from every step — the whole flow is described in one place instead of conditions and flags scattered across components.
  • Two independent processes run at the same time — Loading the data and checking permissions run in parallel and shouldn't interfere with each other, but the final screen depends on how both turn out. Independent processes are described separately, instead of collapsing into one tangled set of flags.
  • Several boolean state flags contradict each other — "Loading," "error," "done" — three separate flags, even though only one of them can really be true at a time. States like these are treated as mutually exclusive from the start, instead of relying on nobody forgetting to reset a stale flag somewhere in the code.

Installation

npm install @macrulez/vue-state-machine

Peer dependency:

npm install vue@>=3.3

Quick start

<script setup lang="ts">
import { defineMachine, useMachine } from '@macrulez/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.

More examples

A machine with context, guards, and actions

A guard blocks the transition once there are already 3 attempts, an action increments the counter and clears the error — the form's logic lives declaratively in one place, not scattered across handlers.

import { defineMachine } from 'vue-state-machine'
import type { Action, Guard } from 'vue-state-machine'

type Ctx = { attempts: number; error: string | null }
type Ev = 'SUBMIT' | 'SUCCESS' | 'FAILURE' | 'RETRY'

const resetError: Action<Ctx, Ev> = () => ({ error: null })
const incrementAttempts: Action<Ctx, Ev> = (ctx) => ({ attempts: ctx.attempts + 1 })
const canRetry: Guard<Ctx, Ev> = (ctx) => ctx.attempts < 3

export const loginMachine = defineMachine<'idle' | 'loading' | 'error' | 'success', Ev, Ctx>({
  id: 'login',
  initial: 'idle',
  context: { attempts: 0, error: null },
  states: {
    idle: { on: { SUBMIT: { target: 'loading', actions: [resetError] } } },
    loading: {
      on: {
        SUCCESS: { target: 'success' },
        FAILURE: { target: 'error', actions: [incrementAttempts] },
      },
    },
    error: { on: { RETRY: { target: 'idle', guard: canRetry } } },
    success: { type: 'final' },
  },
})

Wiring it into a component

send() returns a promise that resolves once the transition finishes, can() synchronously checks whether an event would fire, isDone flips on the final state — all reactive, no manual computed properties.

import { useMachine } from 'vue-state-machine'
import { loginMachine } from './machine'

const { state, context, send, can, isDone } = useMachine(loginMachine)

async function submit() {
  await send('SUBMIT')
  try {
    await api.login()
    send('SUCCESS')
  } catch (e) {
    send({ type: 'FAILURE', message: String(e) })
  }
}

// state.value === 'error'  ->  `Failed. Attempts: ${context.value.attempts}/3`
// can('RETRY')             ->  whether the Retry button should be enabled
// isDone.value             ->  true once login succeeds

A multi-step wizard, no machine of your own

useWizard builds the machine from a steps array on its own — canProceed blocks next() until required fields are filled in, and progress comes ready-made.

import { useWizard } from 'vue-state-machine'
import type { WizardStep } from 'vue-state-machine'

interface CheckoutCtx {
  name: string
  email: string
  address: string
}

const steps: WizardStep<CheckoutCtx>[] = [
  { id: 'info', label: 'Your info', canProceed: (ctx) => !!ctx.name && !!ctx.email },
  { id: 'address', label: 'Delivery', canProceed: (ctx) => !!ctx.address },
]

const { currentStep, progress, next, prev, isLast } = useWizard(steps)

// next() calls canProceed first and returns false if it's blocked — no
// manual validation gate before advancing to the next step.

Documentation & links


License

MIT


💖 Support the project

Open source takes time and effort. If this library saves you time or brings value, consider supporting further development.

Thank you for being part of this journey. ❤️