A pragmatic wrapper around XState with first-class Vue 3 + Vue Router integration.
This package bundles everything you need to centralize view navigation inside a deterministic state machine. It exposes bare utility classes for framework-agnostic usage, plus a Vue plugin that wires the machine into Router navigation guards, snapshot persistence, and ergonomic composition helpers.
- XState 5 powered machine/actor wrapper with validation and error types
- Declarative configuration builder with sensible defaults (
alwaysaction merges params into context) - Persisted snapshots and state definitions via pluggable storage (e.g.,
localStorage) - Optional Vue plugin that:
- Installs a single shared
StateMachineManager - Synchronizes Router navigation (push/replace) through machine transitions
- Guards routes to keep the URL aligned with the active state
- Exposes a
useNavigator()helper for components/composables
- Installs a single shared
- Structured logging with adjustable verbosity for debugging flows
pnpm add state-machine
# or
npm install state-machineThe package ships pre-bundled ESM. Node 18+ / modern bundlers are recommended.
StateMachinewrapscreateMachineand validates configuration before instantiation.Actorencapsulates an XState actor, enforces transition payloads, and surfaces asynctransition()returning the next snapshot.StateMachineManagerorchestrates configuration, persistence, listeners, and state updates.
import { StateMachineManager } from "state-machine";
const states = {
idle: { on: { NEXT: "details" } },
details: { on: { SUBMIT: { target: "confirm", actions: ["save"] } } },
confirm: { on: { RESET: "idle" } },
};
const manager = new StateMachineManager({
states,
entry: "idle",
config: {
actions: {
save: ({ context, event }) => {
// custom side effects
},
},
},
persist: true,
storage: window.localStorage,
logger: { prefix: "[CheckoutSM]", level: 3 },
});
manager.addListeners([
(snapshot) => console.log("state changed →", snapshot.value),
]);
await manager.transition({ type: "NEXT" });Pass a storage implementation (anything exposing getItem, setItem, removeItem) and persist: true to automatically:
- cache merged state definitions (
STATE_MACHINE_CONFIG) - restore the last snapshot (
STATE_MACHINE_SNAPSHOT) - keep snapshots synced whenever they change
Call manager.resetPersistence() or storage.clear() when you need to wipe cached data.
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import StateMachinePlugin from "state-machine";
const router = createRouter({
history: createWebHistory(),
routes: [
{ name: "idle", path: "/idle", component: () => import("./Idle.vue") },
{ name: "details", path: "/details", component: () => import("./Details.vue") },
{ name: "confirm", path: "/confirm", component: () => import("./Confirm.vue") },
],
});
const app = createApp(App);
app.use(StateMachinePlugin, {
states,
entry: "idle",
router,
persist: true,
storage: window.localStorage,
debug: import.meta.env.DEV,
logger: { prefix: "[CheckoutSM]" },
});
app.use(router);
app.mount("#app");export const useNavigator = () => {
return {
...routerIntegration?.createGlobalInterface(),
snapshot,
name: computed(() => extractMostInnerState(snapshot.value)),
useTransitionAllowed: (transition) => {
return computed(() => snapshot.value.can({ type: transition }));
},
};
};- Instantiates a single
StateMachineManager, keeping its snapshot in a global Vueref. - Registers
beforeEachguards to ensure Router routes stay aligned with the active machine state. - Exposes navigation helpers (
push,replace) that automatically serialize transition payloads intoroute.query.stateMachine. - Provides computed helpers for components to read the current state and check
cantransitions reactively.
<script setup>
import { useNavigator, getStateMachine } from "state-machine";
const navigator = useNavigator();
const stateMachine = getStateMachine();
const goNext = async () => {
if (navigator.useTransitionAllowed("NEXT").value) {
await stateMachine.transition({ type: "NEXT", params: { step: 2 } });
await navigator.push({ name: navigator.name.value });
}
};
</script>handleNavigation(to, from) {
// ...
const transitionData = this.extractTransitionData(to);
if (this.isValidTransition(transitionData)) {
this.executeTransition(transitionData, to.name);
}
return this.determineNavigationResult(to);
}- Reads serialized transition data from the route query and fires it through the manager.
- Detects browser back navigation and lets it proceed without cancelling.
- Cancels forward navigation when the requested route does not match the machine's current state, redirecting to the correct one instead.
Use the named export getStateMachine() after the plugin installs to access the shared manager (e.g., inside Pinia stores or standalone modules).
import { getStateMachine } from "state-machine";
const machine = getStateMachine();
machine.transition({ type: "RESET" });The built-in logger (utils/logger.util.js) supports error, warn, info, and debug with a configurable prefix and level. Set debug: false in plugin options to reduce noise in production.
All custom errors inherit from StateMachineError (ConfigurationError, TransitionError, StorageError, ValidationError). These include a context payload and ISO timestamp to make troubleshooting transitions and configuration issues easier.
Need an example or more detail? Open an issue or extend the README with your project-specific flows. Happy state managing!