A minimal, powerful state machine library for TypeScript
- π― Simple - Intuitive API with minimal boilerplate
- π Type-safe - Full TypeScript support with optional generics
- π Zero dependencies - Tiny bundle size
- π Universal - Works in Node.js and browsers
- β‘ Async-first - Built-in support for async handlers
- π Flexible - Manual, automatic, or step-by-step execution
- π Debuggable - Transition events, guards, and history tracking
- πΎ Serializable - Snapshot and restore state
npm install phasestateimport { machine } from 'phasestate';
// Create a state machine
const phaseState = machine("idle", { count: 0 })
.when("idle", {
enter: () => console.log("Ready"),
to: ["active"]
})
.when("active", {
enter: async (ctx) => {
console.log("Processing...");
await doWork();
},
from: ["idle"],
to: ["complete", "error"]
})
.when("complete", {
enter: (ctx) => console.log("Done!", ctx.count),
from: ["active"]
})
.when("error", {
from: "*",
to: ["idle"]
})
.can("active", state => state.context.count < 100);
// Use it
await phaseState.to("active");
phaseState.set({ count: 5 });
// Check available transitions
console.log(phaseState.transitions()); // ['complete', 'error']
// Listen to transitions
phaseState.on(event => {
if ('type' in event && event.type === 'transition') {
console.log(`${event.from} β ${event.to}`);
}
});Create a state machine.
const m = machine("idle");
const m2 = machine("idle", { count: 0 });
const m3 = machine<MyContext>("idle", { data: null });Define state handlers, metadata, and allowed transitions. Returns this for chaining.
m.when("loading", {
enter: async ctx => {
console.log("Started loading");
await fetchData();
},
exit: async ctx => console.log("Stopped loading"),
meta: { cancellable: true, timeout: 5000 },
to: ["success", "error"], // can only transition to these states
from: ["idle", "error"] // can only be entered from these states
// from: "*" // can be entered from any state
});The to and from define valid transitions:
to: States this state can transition tofrom: States that can transition to this state (array or"*"for any). Defaults to all states if omitted.enter/exit: Can be async functionsmeta: Any metadata you want to attach to the state
Transition to a new state. Optionally update context. Checks guards and constraints before transitioning. Returns a Promise.
// Simple transition
await m.to("loading");
// With context update (partial)
await m.to("done", { count: 42 });
// With context update (function)
await m.to("done", ctx => ({ ...ctx, count: ctx.count + 1 }));Add a guard condition that must pass for a transition to occur. Returns this for chaining.
// Only allow loading if not already done
m.can("loading", state => state.value !== "done");
// Guard based on context
m.can("submit", state => state.context.isValid === true);
// Multiple guards can be added
m.can("premium", state => state.context.isPaid && state.context.verified);Return to the previous state. Maintains a history of the last 10 states. Returns a Promise.
await m.to("loading");
await m.to("error");
await m.back(); // returns to "loading"Get valid transitions from the current state. Respects to/from constraints and guards.
const valid = m.transitions(); // ['loading', 'cancelled']
console.log(valid); // states that can be transitioned to right now
// Example
m.when("idle", { to: ["loading", "cancelled"] })
.can("loading", s => s.context.ready);
m.set({ ready: false });
console.log(m.transitions()); // ['cancelled'] - loading blocked by guard
m.set({ ready: true });
console.log(m.transitions()); // ['loading', 'cancelled'] - all allowedCreate a deep copy snapshot of the current state and context.
const snapshot = m.snapshot();
// { state: 'loading', context: { count: 42 } }
// Safe to mutate original without affecting snapshot
m.set({ count: 100 });
console.log(snapshot.context.count); // Still 42Restore state and context from a snapshot. Returns this for chaining.
const snapshot = m.snapshot();
// ... do some transitions ...
m.restore(snapshot); // back to saved stateGet metadata for a state.
const meta = m.meta("loading");
console.log(meta.timeout); // 5000Update context without changing state.
// Partial update
m.set({ count: 5 });
// Function update
m.set(ctx => ({ ...ctx, count: ctx.count + 1 }));Check current state.
if (m.is("loading")) {
console.log("Loading...");
}Subscribe to state changes and transition events. Returns unsubscribe function.
const unsubscribe = m.on(event => {
if ('type' in event && event.type === 'transition') {
// Transition event
console.log(`${event.from} -> ${event.to}`);
if (event.blocked) {
console.log(`Blocked by: ${event.blocked}`);
}
} else {
// State update event
console.log("State:", event.value);
console.log("Context:", event.context);
}
});Get a generator to step through transitions manually.
const steps = m.steps();
steps.next(); // get initial state
steps.next({ to: "loading" });
steps.next({ to: "done", update: { count: 42 } });
steps.next({ to: "idle", update: ctx => ({ ...ctx, count: 0 }) });Run a sequence of transitions automatically. Returns a Promise.
await m.run([
{ to: "loading" },
{ to: "success", delay: 1000 },
{ to: "idle", update: { count: 0 } }
]);Each step can have:
to- Target stateupdate- Context update (partial or function)delay- Optional delay in milliseconds before this transition
.state- Current state name (string).context- Current context object.history- Array of previous states (max 10, readonly)
Clear the state history. Returns this for chaining.
console.log(m.history.length); // 5
m.clearHistory();
console.log(m.history.length); // 0See the examples/ directory for complete, runnable examples:
- π Authentication Flow - Login/logout with session management and locking
- π¦ Traffic Light - Auto-cycling with multiple control modes
- π‘ Data Fetching - API calls with retry logic and snapshots
- π Form Wizard - Multi-step form with validation
Each example demonstrates different features and patterns. View all examples β
Add types for better autocomplete and type safety:
type UserContext = {
id: string;
name: string;
role: 'admin' | 'user';
isAuthenticated: boolean;
};
const user = machine<UserContext>("guest", {
id: "",
name: "Guest",
role: "user",
isAuthenticated: false
});
// TypeScript will enforce context shape
user.set({
name: "Alice",
role: "admin"
}); // β Valid
// TypeScript error:
// user.set({ age: 25 }); // Error: 'age' doesn't exist on UserContext