State Machines in JavaScript
What is a State Machine?
A state machine is a mathematical model that describes an object or system behaving according to a finite number of states. At any given moment, the system exists in exactly one state. It can transition to another state when triggered by specific events.
State machines are everywhere in software: a login form can be in “idle”, “submitting”, “success”, or “error” states. A media player can be “playing”, “paused”, or “stopped”. An order fulfillment system moves through “pending”, “processing”, “shipped”, and “delivered”.
The key benefit is predictability. With explicit states and allowed transitions, you can visualize exactly what can happen and when. There’s no ambiguity about whether a “paused” player can transition directly to “shipped”—it can’t, because that transition isn’t defined.
Core Concepts
Understanding state machines requires grasping five fundamental concepts:
State represents a discrete condition of the system. States are mutually exclusive—a machine is in one state at a time, never multiple. In XState, states are defined as objects within the states property.
Event is a trigger that causes a transition between states. Events are the input to the state machine. When an event occurs, the machine evaluates whether a transition should occur based on its current state.
Transition defines the relationship between states. It specifies which state to move to when a particular event occurs in a particular source state. Transitions are deterministic—the same state and event always produce the same result.
Action is side effect that occurs during a transition. Entry actions run when entering a state. Exit actions run when leaving a state. Transition actions run during the transition itself.
Guard is a conditional check that must evaluate to true for a transition to occur. Guards enable decision-making within the state machine, allowing different transitions from the same event based on current conditions.
XState Basics
XState is the most popular state machine library for JavaScript and TypeScript. It provides a powerful, declarative API for defining and interpreting state machines.
Installing XState
npm install xstate
// value: xstate@5.x installed
Your First State Machine
Here’s a simple toggle machine that switches between “on” and “off” states:
import { createMachine, interpret } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'off',
states: {
off: {
on: { TOGGLE: 'on' }
},
on: {
on: { TOGGLE: 'off' }
}
}
});
const toggleService = interpret(toggleMachine).start();
// value: Machine started in 'off' state
Sending Events
To trigger state transitions, send events to the service:
toggleService.send({ type: 'TOGGLE' });
// value: Machine transitioned to 'on' state
toggleService.send({ type: 'TOGGLE' });
// value: Machine transitioned to 'off' state
The TOGGLE event causes the machine to transition between states based on its current state. This is deterministic—sending TOGGLE from “off” always goes to “on”.
Practical Example: Login Form
Let’s build a login form with explicit states for each phase:
const loginMachine = createMachine({
id: 'login',
initial: 'idle',
states: {
idle: {
on: { SUBMIT: 'submitting' }
},
submitting: {
invoke: {
src: 'performLogin',
onDone: 'success',
onError: 'error'
}
},
success: {
on: { RESET: 'idle' }
},
error: {
on: { RETRY: 'submitting', RESET: 'idle' }
}
}
}, {
services: {
performLogin: (context) => {
return fetch('/api/login', {
method: 'POST',
body: JSON.stringify(context.credentials)
}).then(response => {
if (!response.ok) throw new Error('Login failed');
return response.json();
});
}
}
});
This machine makes the login flow explicit. From “idle”, only “submitting” is reachable. From “submitting”, the only outcomes are “success” or “error”—no mysterious intermediate states. The user can retry from “error” or reset back to “idle”.
Guards and Context
State machines become powerful when you add conditional logic through guards and store data in context.
Adding Context
Context holds the data associated with the machine:
const orderMachine = createMachine({
id: 'order',
initial: 'pending',
context: {
items: [],
total: 0
},
states: {
pending: {
on: { PLACE_ORDER: 'processing' }
},
processing: {
on: {
COMPLETE: {
target: 'completed',
guard: ({ context }) => context.items.length > 0
},
CANCEL: 'cancelled'
}
},
completed: {},
cancelled: {}
}
});
Using Guards
Guards conditionally allow transitions:
const paymentMachine = createMachine({
id: 'payment',
initial: 'awaitingPayment',
context: { balance: 100 },
states: {
awaitingPayment: {
on: {
PROCESS_PAYMENT: [
{ target: 'processing', guard: ({ context }) => context.balance >= 50 },
{ target: 'insufficientFunds' }
]
}
},
processing: {},
insufficientFunds: {
on: { ADD_FUNDS: 'awaitingPayment' }
}
}
});
The PROCESS_PAYMENT event has two possible transitions. The first, with the guard, checks if the balance is sufficient. If true, go to “processing”. Otherwise, fall through to “insufficientFunds”.
Visualization
One of XState’s strongest features is the visualizer. You can paste your machine configuration into the XState Visualizer and see an interactive state diagram.
The diagram shows:
- Boxes for each state
- Arrows for transitions
- Labels for events that trigger transitions
- Guard conditions on transition arrows
This visualization is invaluable for understanding complex flows and communicating with team members. You can spot impossible states at a glance—states that have no incoming transitions, or transitions that seem unnecessary.
When to Use State Machines
State machines shine when:
Complex conditional logic exists. When your code has many if statements checking current state, a state machine makes the logic explicit and visualizable.
You need audit trails. Since transitions are explicit and deterministic, you can log exactly what happened and when.
Multiple actors interact. When several components or users can affect the same workflow, state machines prevent impossible states.
You need testability. Each state and transition can be tested in isolation.
However, don’t reach for state machines for simple boolean flags. A toggle button doesn’t need XState—a simple boolean works fine. The complexity is only worth it when the state space justifies it.
See Also
- Observer Pattern — another behavioral pattern for reactive systems
- Decorator Pattern — structural patterns that modify behavior
- Command Pattern — encapsulating requests as objects