Skip to main content
Case StudiesProjectsAbout
Case StudiesProjectsAbout
Project

Workflow State Machine

Lightweight state machine for complex UI workflows. Visualizer included.

3 min read
TypeScriptXStateReact
100%
Type safety
state + context inferred
Wizard · async · form
Patterns
pre-built machine factories
Live diagram
Debugger
current state + transition history
My Role
Solo: design, engineering
Timeline
2024
Team
1 engineer
Platform
Web
Stack
TypeScript · XState · React · D3 · React DevTools Protocol · Vite
Status
Shipped

Overview

A state machine library that wraps XState with React-specific hooks and utilities, providing a more ergonomic API for common patterns like wizard flows, form submissions, and async operations. Includes a visual debugger that shows the current state, available transitions, and state history in real-time during development.

Motivation

◆Why Not Just Use XState Directly?

XState is powerful but verbose for common patterns. Defining a 4-step wizard requires significant boilerplate. This library provides pre-built machine factories for the most common patterns: you define the states and transitions, not the XState scaffolding.

API

wizard-example.tsx

Pre-built wizard machine factory:

const checkoutMachine = createWizardMachine({
  steps: ["cart", "shipping", "payment", "confirmation"],
  guards: {
    canProceedFromCart: (ctx) => ctx.items.length > 0,
    canProceedFromShipping: (ctx) => !!ctx.shippingAddress,
  },
  onComplete: (ctx) => submitOrder(ctx),
});

// In component:
const { step, next, back, canProceed, isSubmitting } = useWizard(checkoutMachine);

Async operation pattern with automatic state management:

const fetchMachine = createAsyncMachine({
  fetch: (id: string) => api.getUser(id),
  onSuccess: (ctx, user) => ({ ...ctx, user }),
});
// States: idle → loading → success/error, all typed

Visual Debugger

The debugger is a React DevTools extension that subscribes to machine state changes. It renders an interactive state diagram showing:

  • Current active state (highlighted)
  • Available transitions from the current state
  • State history (last 20 transitions)
  • Context diff between transitions

Performance was the main challenge for complex machines. The debugger uses a virtualized rendering approach: only draws visible portions of large state diagrams. A D3-based layout algorithm positions nodes; only nodes within the visible viewport are rendered as React components.

State Persistence

persistence.ts

Custom serializer handles edge cases that break JSON.stringify:

// Standard serialize/deserialize doesn't handle functions in guards or actions
const serializer = createMachineSerializer({
  // Functions are serialized as references, resolved against the machine definition at hydration
  serializeFunction: (fn) => ({ __fnRef: fn.name }),
  deserializeFunction: (ref, machine) => machine.guards[ref.__fnRef],
  // Circular references are tracked via WeakSet during serialization
});

Tech Stack

TypeScript · XState · React · D3 (visualizer layout) · React DevTools Protocol · Vite

PreviousPromptopia

Built with intention, not templates. If you made it this far, thank you for reading thoughtfully.

© 2026 · Designed and built with care