Frontend Development › State Management & Data Fetching
State Machines for UI
Modeling UI as explicit states and transitions, e.g. with XState.
Also known as: state machines in UI, XState, finite state machine UI, statecharts, UI statecharts
A state machine models behavior as a finite set of explicit states and the events that move between them. Applied to UI, it replaces a tangle of booleans and conditionals with a clear picture of what the interface can be, and what’s allowed to happen next.
The typical mess:
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// What does isLoading=true + isError=true mean? Nothing valid. But the code allows it.
With a state machine, the UI is in exactly one state at a time:
SUBMIT SUCCESS
idle ───────────────► submitting ───────────► success
▲ │
│ RETRY │ FAILURE
└──────────────── error ◄┘
type State =
| { status: "idle" }
| { status: "submitting" }
| { status: "success"; orderId: string }
| { status: "error"; message: string };
function reducer(state: State, event: Event): State {
switch (state.status) {
case "idle": return event.type === "SUBMIT" ? { status: "submitting" } : state;
case "submitting":
if (event.type === "SUCCESS") return { status: "success", orderId: event.orderId };
if (event.type === "FAILURE") return { status: "error", message: event.message };
return state;
case "error": return event.type === "RETRY" ? { status: "submitting" } : state;
default: return state;
}
}
This uses a discriminated union so impossible combinations can’t be represented.
Why it helps
- Impossible states are impossible: no “loading and error at once”.
- All transitions are visible: you can see (and draw) every way to move between states, which makes review and testing easy.
- Events are ignored when not valid: a second submit click during
submittingdoes nothing, because that state has noSUBMITtransition (double submits). - Easier to reason about complex flows: multi-step wizards, drag and drop, media players, authentication flows, form lifecycles.
- Testable: test transitions, independent of the UI.
- Communication: the diagram works as a shared language with designers and product people.
Libraries and tools
A reducer (reducers) with a status field is already a lightweight state machine. For larger or hierarchical flows, libraries such as XState add statecharts (nested and parallel states, guards, delayed transitions, actors) and visualization tools.
When to use it
- Flows with several steps or modes, where the rules for what’s allowed depend on the current mode.
- When you notice boolean flags multiplying (
isOpen,isEditing,isSaving,hasError). - Async flows where requests, retries and cancellations interact (loading, error and empty states).
When not to
A simple toggle or a single piece of state doesn’t need one. Don’t add a library for two states. Use the pattern (a status union and a reducer) before the framework. See finite state machines.