# 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 ; if (state.matches("error")) return

{state.context.error}

; return ; } ``` `setup()` declares the `loadItems` actor once; `loading` invokes it via `invoke.src`. On `onDone`, `assign` writes `event.output` to context. React reads only `state.value` and `state.context` — async logic stays in the machine. ## Tradeoffs **Pros** - Impossible states become unrepresentable — no `isLoading && isError` bugs. - Transitions are explicit; any developer can read the machine and know every valid path. - XState actors isolate async side effects from render logic, making them independently testable. - `useReducer` machines need zero dependencies and add no bundle weight. - Statecharts visualise directly in Stately Studio for design–dev handoff. **Cons** - XState v5 adds ~15 KB min+gzip; overkill for simple flag toggles. - Setup cost is higher than ad-hoc state — requires upfront state enumeration. - Teams unfamiliar with statechart notation must learn `invoke`, `entry`/`exit`, and guard semantics. - `setup().createMachine()` verbosity can feel heavy for a two-state toggle. ## Key terms - **finite state machine**: A model with a fixed set of states, one active at a time, and explicit transitions between them. - **statechart**: An extended state machine with hierarchy, parallel regions, and entry/exit actions. - **discriminated union**: A TypeScript union where a shared `type` or `status` field narrows each branch. - **actor (XState)**: A running process that receives events, holds state, and can spawn child actors. - **guard**: A boolean function on context and event that conditionally allows a transition. ## Related topics - [Client State Management](https://fearchitect.com/topics/client-state-management.md): Decide where UI state lives; pick the right tool for the scope. - [Optimistic UI & Mutations](https://fearchitect.com/topics/optimistic-ui-mutations.md): Update the UI before the server replies; roll back on error. - [Error Boundaries & Resilience](https://fearchitect.com/topics/error-boundaries-resilience.md): Isolate render failures so one widget can't crash the page. - [Signals & Fine-Grained Reactivity](https://fearchitect.com/topics/signals-fine-grained-reactivity.md): Observable values that re-run only their exact dependents. - [Rich Text Editor](https://fearchitect.com/topics/rich-text-editor.md): Own a document model; never trust raw contentEditable output. - [Real-time: WebSockets vs SSE vs Polling](https://fearchitect.com/topics/realtime-websockets-sse-polling.md): Match the right real-time transport to your data-flow direction. ## Further reading - [XState v5 — Machines](https://stately.ai/docs/machines) - [XState v5 — Actors](https://stately.ai/docs/actors) - [XState v5 — setup()](https://stately.ai/docs/setup) - [XState v5 is here (release post)](https://stately.ai/blog/2023-12-01-xstate-v5) - [React docs — useReducer](https://react.dev/reference/react/useReducer)