XState State Machines
XState for state machines and statecharts in React — actors, guards, actions, services, @xstate/react integration, and the visual editor
XState models application logic as finite state machines and statecharts. Instead of tracking booleans and flags that can combine into impossible states, you define explicit states and the transitions between them. Every possible state is declared upfront; every transition is intentional. This eliminates entire categories of bugs — "loading and error at the same time," "submitted but still editable," or "idle but showing a spinner." ## Key Points - **Explicit states** — every possible state is named and accounted for; no implicit flag combinations - **Event-driven transitions** — state changes only happen through defined events - **Guards and actions** — conditional transitions and side effects are colocated with the machine definition - **Actors model** — machines can spawn child actors for parallel and hierarchical logic - **Visual editor** — Stately.ai provides a drag-and-drop editor that generates XState code - **Framework-agnostic** — works with React, Vue, Svelte, or vanilla JS via adapters - Renders statecharts as interactive diagrams - Generates XState v5 code that you paste into your project - Supports drag-and-drop state creation, transition drawing, and guard/action configuration - Syncs with GitHub repos for live visualization of existing machines - Simulates machine execution step by step for debugging 1. **Model states explicitly** — if your UI has "loading," "error," and "success" modes, make them states in the machine, not boolean flags. ## Quick Example ```bash npm install xstate @xstate/react ```
skilldb get state-management-skills/XState State MachinesFull skill: 470 linesXState State Machines & Statecharts
Core Philosophy
XState models application logic as finite state machines and statecharts. Instead of tracking booleans and flags that can combine into impossible states, you define explicit states and the transitions between them. Every possible state is declared upfront; every transition is intentional. This eliminates entire categories of bugs — "loading and error at the same time," "submitted but still editable," or "idle but showing a spinner."
- Explicit states — every possible state is named and accounted for; no implicit flag combinations
- Event-driven transitions — state changes only happen through defined events
- Guards and actions — conditional transitions and side effects are colocated with the machine definition
- Actors model — machines can spawn child actors for parallel and hierarchical logic
- Visual editor — Stately.ai provides a drag-and-drop editor that generates XState code
- Framework-agnostic — works with React, Vue, Svelte, or vanilla JS via adapters
Setup
npm install xstate @xstate/react
// machines/toggleMachine.ts
import { createMachine } from 'xstate';
export const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {
on: { TOGGLE: 'active' },
},
active: {
on: { TOGGLE: 'inactive' },
},
},
});
// components/Toggle.tsx
import { useMachine } from '@xstate/react';
import { toggleMachine } from '../machines/toggleMachine';
export function Toggle() {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send({ type: 'TOGGLE' })}>
{state.matches('active') ? 'ON' : 'OFF'}
</button>
);
}
Key Techniques
Context (Extended State)
import { createMachine, assign } from 'xstate';
interface CounterContext {
count: number;
max: number;
}
type CounterEvent =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'RESET' };
export const counterMachine = createMachine({
id: 'counter',
types: {} as {
context: CounterContext;
events: CounterEvent;
},
initial: 'active',
context: {
count: 0,
max: 10,
},
states: {
active: {
on: {
INCREMENT: {
guard: ({ context }) => context.count < context.max,
actions: assign({ count: ({ context }) => context.count + 1 }),
},
DECREMENT: {
guard: ({ context }) => context.count > 0,
actions: assign({ count: ({ context }) => context.count - 1 }),
},
RESET: {
actions: assign({ count: 0 }),
},
},
},
},
});
Complex Machine — Authentication Flow
import { createMachine, assign } from 'xstate';
interface AuthContext {
user: { id: string; name: string; email: string } | null;
error: string | null;
attempts: number;
}
type AuthEvent =
| { type: 'LOGIN'; email: string; password: string }
| { type: 'LOGOUT' }
| { type: 'RETRY' };
export const authMachine = createMachine({
id: 'auth',
types: {} as {
context: AuthContext;
events: AuthEvent;
},
initial: 'idle',
context: {
user: null,
error: null,
attempts: 0,
},
states: {
idle: {
on: {
LOGIN: 'authenticating',
},
},
authenticating: {
entry: assign({ error: null }),
invoke: {
id: 'loginService',
src: 'loginUser',
input: ({ event }) => {
if (event.type === 'LOGIN') {
return { email: event.email, password: event.password };
}
return {};
},
onDone: {
target: 'authenticated',
actions: assign({
user: ({ event }) => event.output,
attempts: 0,
}),
},
onError: {
target: 'error',
actions: assign({
error: ({ event }) => (event.error as Error).message,
attempts: ({ context }) => context.attempts + 1,
}),
},
},
},
authenticated: {
on: {
LOGOUT: {
target: 'idle',
actions: assign({ user: null }),
},
},
},
error: {
on: {
RETRY: {
target: 'idle',
guard: ({ context }) => context.attempts < 3,
},
LOGIN: 'authenticating',
},
},
},
});
Guards (Conditional Transitions)
import { createMachine, assign } from 'xstate';
interface FormContext {
email: string;
password: string;
}
export const loginFormMachine = createMachine({
id: 'loginForm',
types: {} as {
context: FormContext;
events:
| { type: 'SET_EMAIL'; value: string }
| { type: 'SET_PASSWORD'; value: string }
| { type: 'SUBMIT' };
},
initial: 'editing',
context: { email: '', password: '' },
states: {
editing: {
on: {
SET_EMAIL: {
actions: assign({ email: ({ event }) => event.value }),
},
SET_PASSWORD: {
actions: assign({ password: ({ event }) => event.value }),
},
SUBMIT: [
{
target: 'submitting',
guard: ({ context }) =>
context.email.includes('@') && context.password.length >= 8,
},
{
target: 'invalid',
},
],
},
},
invalid: {
on: {
SET_EMAIL: {
target: 'editing',
actions: assign({ email: ({ event }) => event.value }),
},
SET_PASSWORD: {
target: 'editing',
actions: assign({ password: ({ event }) => event.value }),
},
},
},
submitting: {
// invoke submission service here
},
},
});
Hierarchical (Nested) States
export const playerMachine = createMachine({
id: 'player',
initial: 'stopped',
states: {
stopped: {
on: { PLAY: 'playing' },
},
playing: {
initial: 'normal',
on: {
STOP: 'stopped',
},
states: {
normal: {
on: { FAST_FORWARD: 'fastForward' },
},
fastForward: {
on: { NORMAL: 'normal' },
},
},
},
},
});
// Check nested states
// state.matches('playing') -> true when in any playing substate
// state.matches('playing.normal') -> true only in normal substate
// state.matches('playing.fastForward') -> true only in fast forward
Parallel States
export const editorMachine = createMachine({
id: 'editor',
type: 'parallel',
states: {
bold: {
initial: 'off',
states: {
off: { on: { TOGGLE_BOLD: 'on' } },
on: { on: { TOGGLE_BOLD: 'off' } },
},
},
italic: {
initial: 'off',
states: {
off: { on: { TOGGLE_ITALIC: 'on' } },
on: { on: { TOGGLE_ITALIC: 'off' } },
},
},
underline: {
initial: 'off',
states: {
off: { on: { TOGGLE_UNDERLINE: 'on' } },
on: { on: { TOGGLE_UNDERLINE: 'off' } },
},
},
},
});
// state.matches({ bold: 'on', italic: 'off' }) -> checks parallel substates
React Integration with @xstate/react
import { useMachine, useActor } from '@xstate/react';
import { authMachine } from '../machines/authMachine';
function LoginPage() {
const [state, send] = useMachine(authMachine, {
actors: {
loginUser: fromPromise(async ({ input }) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(input),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) throw new Error('Invalid credentials');
return res.json();
}),
},
});
if (state.matches('authenticated')) {
return (
<div>
<p>Welcome, {state.context.user?.name}</p>
<button onClick={() => send({ type: 'LOGOUT' })}>Logout</button>
</div>
);
}
return (
<form
onSubmit={(e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
send({
type: 'LOGIN',
email: fd.get('email') as string,
password: fd.get('password') as string,
});
}}
>
<input name="email" type="email" required />
<input name="password" type="password" required />
{state.matches('error') && (
<p className="error">{state.context.error}</p>
)}
<button type="submit" disabled={state.matches('authenticating')}>
{state.matches('authenticating') ? 'Signing in...' : 'Sign in'}
</button>
{state.matches('error') && state.context.attempts < 3 && (
<button type="button" onClick={() => send({ type: 'RETRY' })}>
Retry
</button>
)}
</form>
);
}
Actors (XState v5)
import { createMachine, fromPromise, assign } from 'xstate';
// Define an actor from a promise
const fetchUserActor = fromPromise(
async ({ input }: { input: { userId: string } }) => {
const res = await fetch(`/api/users/${input.userId}`);
if (!res.ok) throw new Error('User not found');
return res.json();
}
);
// Use the actor in a machine
export const profileMachine = createMachine({
id: 'profile',
initial: 'loading',
context: { user: null as User | null },
states: {
loading: {
invoke: {
src: fetchUserActor,
input: ({ event }) => ({ userId: '123' }),
onDone: {
target: 'loaded',
actions: assign({ user: ({ event }) => event.output }),
},
onError: 'error',
},
},
loaded: {
on: { REFRESH: 'loading' },
},
error: {
on: { RETRY: 'loading' },
},
},
});
useSelector for Performance
import { useMachine, useSelector } from '@xstate/react';
function AuthStatus({ actorRef }: { actorRef: ActorRefFrom<typeof authMachine> }) {
// Only re-renders when the selected value changes
const isAuthenticated = useSelector(actorRef, (state) =>
state.matches('authenticated')
);
const userName = useSelector(
actorRef,
(state) => state.context.user?.name
);
return isAuthenticated ? <span>{userName}</span> : <span>Guest</span>;
}
Visual Editor (Stately.ai)
XState machines can be designed visually at stately.ai/editor. The visual editor:
- Renders statecharts as interactive diagrams
- Generates XState v5 code that you paste into your project
- Supports drag-and-drop state creation, transition drawing, and guard/action configuration
- Syncs with GitHub repos for live visualization of existing machines
- Simulates machine execution step by step for debugging
To visualize an existing machine, import it in the Stately editor or use the VS Code extension.
Best Practices
- Model states explicitly — if your UI has "loading," "error," and "success" modes, make them states in the machine, not boolean flags.
- Use
state.matches()over string comparison — it handles nested and parallel states correctly. - Keep machines pure — define side effects as actions and services, not inline in the machine config.
- Use the
typesproperty for TypeScript — declare context and event types viatypes: {} as { context: ...; events: ... }. - Test machines without React — machines are pure logic; test transitions with
machine.transition(state, event)in unit tests. - Use
useSelectorfor granular subscriptions — avoids re-rendering the entire component on every state change. - Visualize early — sketch the statechart in the Stately editor before writing code to catch missing states and transitions.
Anti-Patterns
- Boolean soup instead of states —
isLoading && !isError && !isSuccessis exactly what state machines prevent; use explicit states. - Unreachable states — states without any incoming transitions are dead code; the visual editor helps catch these.
- Side effects in guards — guards should be pure predicates; use actions for side effects.
- Overly granular machines — not everything needs a state machine; simple toggles or counters work fine with
useState. - Ignoring the actor model — spawning child machines for independent concerns (like each form field) avoids monolithic machines.
- Skipping the visual editor — complex machines become hard to reason about from code alone; always visualize non-trivial machines.
Install this skill directly: skilldb add state-management-skills
Related Skills
Jotai Atomic State Management
Jotai atomic state for React — primitive/derived/async atoms, Provider-less mode, atomWithStorage, atomWithQuery, and React Suspense integration
Legend State
Legend-State high-performance observable state for React — fine-grained reactivity, persistence plugins, computed observables, and sync engine
Nanostores
Nanostores tiny framework-agnostic state manager — atoms, computed stores, lifecycle events, and integrations with React, Vue, Svelte, and vanilla JS
Redux Toolkit
Redux Toolkit for scalable React state — createSlice, configureStore, RTK Query, createAsyncThunk, entity adapter, middleware, and TypeScript patterns
TanStack Query (React Query)
TanStack Query for server state — useQuery, useMutation, query invalidation, optimistic updates, infinite queries, prefetching, and SSR hydration
Valtio Proxy State Management
Valtio proxy-based state for React — mutable-style API with automatic tracking, snapshots, derived state, and nested object support