# State Machines
> Model UI as explicit states with typed transitions to kill impossible states.
Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/state-machines
Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end.
## Summary
Boolean flag soup (`isLoading && isError`) lets the UI enter states that can't exist in reality. A state machine makes every valid state and transition explicit, so the impossible becomes unrepresentable. Use `useReducer` for simple cases; reach for XState v5 (`setup().createMachine()`) when you need hierarchical states, side effects as actors, or visual tooling.
## Definition
A state machine defines a finite set of states (`idle`, `loading`, `success`, `error`) and the events that move between them. At any moment the machine is in exactly one state.
The classic failure mode: four booleans (`isLoading`, `isError`, `isEmpty`, `hasData`) can represent 16 combinations, but only 4 are valid. The other 12 are bugs.
Model instead as a discriminated union — `{ status: "loading" | "error" | "empty" | "success"; data?: T }` — and the invalid combinations become type errors.
A **statechart** extends machines with hierarchy (sub-states) and parallel regions, making multi-step forms or media players tractable.
## Diagram
```mermaid
stateDiagram-v2
[*] --> idle
idle --> loading : FETCH
loading --> success : onDone
loading --> error : onError
error --> loading : RETRY
success --> loading : REFRESH
success --> idle : RESET
```
Four states, six transitions — every arrow is explicit code; the 12 impossible flag combinations never exist.
## XState v5 — setup + createMachine + useMachine
Two approaches: `useReducer` — discriminated-union state, event union, pure reducer; TypeScript exhaustiveness catches missing transitions. XState v5 — call `setup({ types, actors })` then chain `.createMachine()`; async work becomes a `fromPromise` actor invoked from a state, transitioning on `onDone`/`onError`.
**XState v5 — setup + createMachine + useMachine in React**
```tsx
import { assign, setup, fromPromise } from "xstate";
import { useMachine } from "@xstate/react";
const fetchMachine = setup({
types: {
context: {} as { data: string[]; error: string },
events: {} as { type: "FETCH" } | { type: "RESET" },
},
actors: {
loadItems: fromPromise(async () => fetchItems()),
},
}).createMachine({
id: "fetch",
initial: "idle",
context: { data: [], error: "" },
states: {
idle: {
on: { FETCH: "loading" },
},
loading: {
invoke: {
src: "loadItems",
onDone: {
target: "success",
actions: assign({ data: ({ event }) => event.output }),
},
onError: {
target: "error",
actions: assign({ error: ({ event }) => (event.error as Error).message }),
},
},
},
success: {
on: { RESET: "idle" },
},
error: {
on: { FETCH: "loading" },
},
},
});
function DataLoader() {
const [state, send] = useMachine(fetchMachine);
if (state.matches("idle")) return ;
if (state.matches("loading")) return
{state.context.error}
; return