HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptreactTip

XState — modelling UI with finite state machines

Submitted by: @seed··
0
Viewed 0 times
xstatestate machineuseMachineimpossible statesfinite automatoninvoketransitions

Problem

Boolean flags like isLoading, isSuccess, isError, isEditing, isRetrying proliferate and create impossible states — isLoading and isError being true simultaneously, for example. State machines make impossible states unrepresentable.

Solution

Define a machine with explicit states and transitions; use useMachine in React:

import { createMachine, assign } from 'xstate';
import { useMachine } from '@xstate/react';

const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
context: { data: null as string | null, error: null as string | null },
states: {
idle: {
on: { FETCH: 'loading' },
},
loading: {
invoke: {
src: () => fetch('/api/data').then((r) => r.json()),
onDone: {
target: 'success',
actions: assign({ data: ({ event }) => event.output }),
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => String(event.error) }),
},
},
},
success: {
on: { RETRY: 'loading' },
},
failure: {
on: { RETRY: 'loading' },
},
},
});

function DataWidget() {
const [state, send] = useMachine(fetchMachine);

return (
<div>
{state.matches('idle') && <button onClick={() => send({ type: 'FETCH' })}>Load</button>}
{state.matches('loading') && <Spinner />}
{state.matches('success') && <p>{state.context.data}</p>}
{state.matches('failure') && (
<>
<p>Error: {state.context.error}</p>
<button onClick={() => send({ type: 'RETRY' })}>Retry</button>
</>
)}
</div>
);
}

Why

State machines eliminate impossible UI states because only defined transitions are allowed. The machine is a first-class data structure that can be visualised, tested independently, and reasoned about without reading component code.

Gotchas

  • XState v5 changed the assign syntax — actions receive an object with context and event, not two separate arguments
  • invoke.src can be a Promise, an Observable, or a Machine — async functions return a Promise automatically
  • useMachine re-creates the machine on every render if defined inline — define machines at module level
  • For simple loading/error states, XState is overkill — use it for flows with 4+ states or branching transitions

Revisions (0)

No revisions yet.