xstate
Model front-end and workflow logic as XState v5 statecharts and actors — when finite states earn their place (and when @xstate/store or a discriminated union is honest), event-first machine design, guards/actions/delays, invoke vs spawn, app-level actor systems, persistence, @xstate/react integration, behavior-driven machine testing including model-based path generation via xstate/graph, and a Mermaid stateDiagram-v2 render regenerated whenever a machine is designed or changed. Fires on ordinary React code that is already a hand-rolled statechart, not only on files that import xstate — a submitting or isLoading useState, a then/catch chain that sets state in sequence, a double-submit guard such as an early return while submitting, an error cleared just before a retry, a useEffect needing an ignore flag in its cleanup, a timer something must clear, or a drag or gesture handler binding document listeners it must unbind. Two tests decide ownership, never how the value renders: an external answer changed it, or the interaction has an interruptible middle. A disabled button or spinner is the presentation of temporal state, not presentation state. State stays in React only when both tests come back empty — one event sets it completely, with no phase to interrupt and nothing to dispose. Use when building or reviewing wizards, checkout/auth/upload flows, async orchestration with retries or timeouts, canvas and editor interactions, anything sent to a BFF or server, boolean-flag state bugs, or any XState or state-machine code. For general test patterns see testing; for component tests see react-testing; for store-only state see the when-to-model reference first.
Works with
---
name: xstate
description: Model front-end and workflow logic as XState v5 statecharts and actors — when finite states earn their place (and when @xstate/store or a discriminated union is honest), event-first machine design, guards/actions/delays, invoke vs spawn, app-level actor systems, persistence, @xstate/react integration, behavior-driven machine testing including model-based path generation via xstate/graph, and a Mermaid stateDiagram-v2 render regenerated whenever a machine is designed or changed. Fires on ordinary React code that is already a hand-rolled statechart, not only on files that import xstate — a submitting or isLoading useState, a then/catch chain that sets state in sequence, a double-submit guard such as an early return while submitting, an error cleared just before a retry, a useEffect needing an ignore flag in its cleanup, a timer something must clear, or a drag or gesture handler binding document listeners it must unbind. Two tests decide ownership, never how the value renders: an external answer changed it, or the interaction has an interruptible middle. A disabled button or spinner is the presentation of temporal state, not presentation state. State stays in React only when both tests come back empty — one event sets it completely, with no phase to interrupt and nothing to dispose. Use when building or reviewing wizards, checkout/auth/upload flows, async orchestration with retries or timeouts, canvas and editor interactions, anything sent to a BFF or server, boolean-flag state bugs, or any XState or state-machine code. For general test patterns see testing; for component tests see react-testing; for store-only state see the when-to-model reference first.
license: MIT
---
# XState
State machines and statecharts (XState v5) turn front-end flow logic from scattered event handlers into one explicit, verifiable model. This skill exists because that trade is unusually good in AI-assisted development:
- **Headless testability** — a machine is events in, snapshots out. Flow logic is proven with fast unit tests before any UI exists, and `xstate/graph` can generate covering paths through every transition.
- **Impossible states are unrepresentable** — finite states plus guarded transitions delete the boolean-flag bug class (`isLoading && isError`); the model forbids drift instead of relying on discipline.
- **One reviewable artifact** — every state × event combination lives in a single declarative structure, so agents and reviewers reason about the whole behavior surface at once.
- **Humans get the diagram for free** — Stately Studio and the inspector render exactly the machine that ships; review becomes "look at the chart".
**Default stance**: model front-end *flow logic* — wizards, checkout, auth, uploads, anything with modes, sequencing, cancellation, timeouts, retries — as statecharts. Keep trivial state honest: data-shaped state with no modes goes to `@xstate/store` or a plain discriminated union, never a ceremonial machine.
**The failure mode this skill exists to catch is under-modeling, not over-modeling.** Model training data is overwhelmingly ordinary React — `useState` and `useEffect` for everything — so the realistic risk is quietly hand-rolling a statechart, not building a ceremonial one. Read [Before You Write State](#before-you-write-state) *before* reaching for `useState`, not after deciding to use XState.
**Version guardrail**: XState **v5** only (`setup()`, `createActor`, actors). Most tutorials, old blog posts, and model training data are v4 — if you see `Machine()`, `interpret()`, `cond:`, `services:`, or typegen, you are looking at v4; translate via the migration table before any of it ships.
**Load alongside**: `testing` (behavior-driven patterns and factories), `react-testing`/`front-end-testing` (component-level tests), `typescript-strict` (schema-first at trust boundaries — external events get parsed before `send`), `functional` (context is immutable data). For event-sourced *domain* state, `event-sourcing` owns the Decider; an XState machine may drive a UI flow in front of it, but the event log is not machine context.
Read the relevant reference before deciding:
| Reference | Read when... |
|-----------|--------------|
| [`references/when-to-model.md`](references/when-to-model.md) | Deciding machine vs store vs union — the complexity ladder, flag-smell signals, finite-state vs context split, store→machine migration, where machines live |
| [`references/machine-design.md`](references/machine-design.md) | Designing a machine — event-first workflow, naming, hierarchy/parallel/history/final, transition and re-entry semantics, actions, guards, delays, anti-pattern catalog |
| [`references/actors-and-systems.md`](references/actors-and-systems.md) | Effects and composition — actor logic types, invoke vs spawn, spawn hygiene, communication, `systemId` receptionist, persistence, inspection |
| [`references/react-integration.md`](references/react-integration.md) | Wiring to React — hook selection, `createActorContext`, StrictMode, `@xstate/store`, production pitfalls |
| [`references/testing.md`](references/testing.md) | Testing — the layered split, `provide()` as the seam, SimulatedClock, pure `transition`, model-based testing, persistence round-trips, mutation-score guidance |
| [`references/typescript-and-migration.md`](references/typescript-and-migration.md) | Types and versions — `setup()` typing model, `assertEvent`, type helpers, no-typestates caveat, full v4→v5 table |
## Before You Write State
This section fires on ordinary React code. You do not have to be in a machine file — or have decided to use XState at all — for it to apply.
### Step 0: Find out who owns machines in this repository
Before creating a machine, moving state into one, or deciding where a new actor lives, check what this repository already says: architecture or boundary tests, import-boundary rules such as `eslint-plugin-boundaries` or `dependency-cruiser`, architecture decision records, the directory already holding existing machines, `CLAUDE.md`, or the layout guidance in `structure-codebase`. Repository convention outranks this skill's default colocation advice.
A correct machine in a forbidden layer still fails the build. Establish the allowed home *before* writing, not after CI rejects it.
### If you just wrote one of these, stop — you are already at step 1
Each of these is a hand-rolled statechart: the states exist, they are just implicit, scattered, and unverified. Treat any single hit as the trigger to work through this skill.
| What you wrote | The statechart hiding in it |
|----------------|------------------------------|
| `useState` for `submitting`, `isLoading`, `saving`, `pending`, `inFlight` | A `submitting` state, with `SUBMIT` illegal while it holds |
| `.then(...)/.catch(...)` or `await` followed by two or three `setX` calls in sequence | Entry actions of the success and failure states |
| A double-submit guard — an early return, a disabled flag checked before dispatching | A transition the model would simply not define |
| Clearing an error immediately before retrying | A `retrying` transition that re-enters the invoking state |
| `useEffect` watching a flag to trigger the next step | A transition trying to escape from an effect |
| Two booleans that can contradict (`isLoading && isError`) | n booleans = 2ⁿ combinations, of which a handful are legal |
| A `status` string that handlers keep re-deriving from other fields | The finite state, denormalized |
| A ref or timer coordinating cancellation, debounce, or staleness by hand | Actor lifetime, `after`, and cancellation semantics |
| A `useEffect` that starts async work and needs an ignore flag in its cleanup | Actor lifetime — the machine cancels instead of ignoring |
| A `setTimeout`/`setInterval` for retry, debounce, or polling that something must clear | `after`, with cancellation tied to the state that owns it |
| A `pointerdown`, `dragstart`, or gesture handler binding document listeners it must unbind | An interaction with an interruptible middle — an invoked actor owns the listeners and disposes them |
| Handling for a response arriving after cancel, replacement, or unmount | Stale-result fencing by identity, which the machine makes structural |
Any one of them is the entry point, **however small the request**, and whether or not the file imports `xstate`.
### The two lifetime tests
Classify state by what these two tests show, never by what the value renders. Run both over every `useState` you write or keep.
**Test 1 — did an external answer change it?** A network response, a timer, another actor, or the platform makes state temporal whatever it looks like on screen. **A disabled button or a spinner is the *presentation of* temporal state, not presentation state.** Anything sent to a BFF or server, and everything the answer implies, is caught here.
**Test 2 — does the interaction have an interruptible middle?** Direct user input still creates a lifecycle when there is a phase between start and end that something can interrupt, cancel, or must dispose. A drag holds pointer capture, cancels on Escape, and must release on unmount. A panel that animates shut has a closing phase. A gesture that binds document listeners must unbind them.
**State belongs to React only when both tests come back empty** — one event sets the value completely, leaving no phase to interrupt and nothing to dispose. A toolbar toggle, a controlled input's current text, a hover, a focus flag, a plain open/closed disclosure: one event, no middle, nothing to clean up.
| Example | Test 1 | Test 2 | Owner |
|---------|--------|--------|-------|
| `submitting` around a command | Yes — the response clears it | — | Machine |
| Spinner visibility | Yes — presentation *of* temporal state | — | Machine |
| Canvas node drag | No | Yes — capture, Escape, unmount | Machine |
| Panel with a closing animation | No | Yes — a closing phase | Machine |
| Toolbar open/closed | No | No | React |
| Controlled input text | No | No | React |
Test 2 is the one most often missed, because a drag is user-driven and test 1 alone would wrongly file it in React.
### State the verdict, and ask on genuine ties
Having run both tests, act on the answer and report the call in one line so the user can overrule it: *"`isSubmitting` is set by the request, so it moved into the machine as `submitting`."*
Where it is genuinely close — a real but small lifecycle, or a machine the repository's layer rules would push far from the component that uses it — **put the choice to the user before building**: name the state, name the phase that makes it a lifecycle, and ask whether it belongs in the machine. A tie is worth one question; it is not worth a silent guess in either direction.
### Ask whether an existing actor already owns this flow
The complexity ladder in `when-to-model.md` answers a *greenfield* question. It is the wrong tool when a machine for this flow already exists.
**If an actor already owns the lifecycle this state belongs to, the state goes in that actor — the ladder does not reopen the decision.** A submission flag next to a machine that already sends the command, handles its errors, and owns the retry policy is not a fresh rung-1 decision; it is one state that escaped its machine. Splitting a single lifecycle across a machine and a component's `useState` gives you two sources of truth that can disagree, which is the exact bug class the machine was adopted to delete.
Before adding flow state to a component, search the feature for an existing machine, actor, or `createActorContext` provider and check whether the lifecycle you are about to hand-roll is one it already models.
When a new machine *is* justified, the boundary is **a distinct unit of functionality with its own inputs and outputs** — typically its own connection, resource, or authority. In a real actor-based front end that produced one machine per SSE stream, one for canvas interaction, one for the workshop's node-adding rules, and one for media-connection management: each owns a different external edge, so each fails and reconnects independently.
The question to answer explicitly, and out loud, is not "does this deserve a machine?" but **"why does this not belong to the machine that already owns this edge?"** A small new machine sitting beside a larger one that shares its inputs and outputs is usually a state that should have been added to the larger one. Where the answer is genuinely close, ask rather than guess.
### Sweeping for the same violation elsewhere
Once one hand-rolled statechart is found, the same pattern is usually repeated across the feature. To sweep: grep the feature for the smell list above — `useState` initialized to `false` near an `await`, `setError(null)` before a call, `if (<flag>) return` at the top of a handler — then, for each hit, apply the cause-not-appearance table. Report each as *keep in React* or *belongs in machine X*, with the reason stated as what changes the value. Fix them as separate increments, each with its own failing machine test, rather than one sweeping commit.
## The Rules That Prevent Most XState Bugs
1. **Events first, then modes.** List what the world can do; states fall out of "when does the same event mean something different?" Never design states off the UI tree.
2. **Finite state = mode, context = data.** If a value gates which events are legal, it is a state; if it is unbounded, it is context. A context flag checked by guards on every transition is a state in hiding — promote it. Classify by what *changes* a value, never by what it renders.
3. **Context is JSON data.** No class instances, promises, or DOM nodes — persistence, inspection, and the editor all depend on it. Spawned actor refs are the sanctioned exception.
4. **Effects are actors, not actions.** Anything with a result, a lifetime, or a failure mode is invoked or spawned; actions are fire-and-forget. `invoke` when the lifetime belongs to a state, `spawn` for dynamic counts — and every spawn has an owner that stops it.
5. **Name everything in `setup()`.** Named actions, guards, actors, and delays make string references type-checked, the chart readable, and `machine.provide()` the universal test seam.
6. **All v5 transitions are internal by default.** Targetless self-transitions for "update and stay"; `reenter: true` only when you truly want exit/entry re-run and invocations restarted.
7. **Machines complete like functions.** Flows that end declare final states and `output`; parents consume `onDone`/`onError`. Error paths are modeled states, not console noise.
8. **One machine per flow — and one owner per lifecycle.** Cross-flow coordination is an actor system (parent machines, `systemId`), never a god machine. Equally, a lifecycle already owned by an actor does not get a second copy of itself in a component's `useState`.
9. **Test the machine headlessly, the component through the DOM.** The snapshot is the machine's contract; it is implementation detail one level up. Never assert machine state from a component test.
10. **Validate at the trust boundary.** Events from outside the process (sockets, storage, URLs) are schema-parsed before `send`; persisted snapshots are restored only with a versioning story.
## Rendering the Machine as a Diagram
A statechart's main advantage over scattered handlers is that a human can *look* at it — which is worth nothing if the picture and the machine have drifted apart.
**Render or update the diagram whenever a machine is designed or changed, even when nobody asked for one**, and render on request at any time. The cost of a diagram is small; the cost of a stale one is a reviewer trusting a picture that no longer describes the code. Regenerating on every change is what keeps that from happening, so it is not optional work at the end — it is part of changing the machine.
Derive it from the machine source, never from memory of it, and regenerate from the *final* definition once the change is complete. Report the validation result honestly: say whether the diagram was checked to render, and never claim a render that was not performed.
Render as Mermaid `stateDiagram-v2`:
```mermaid
stateDiagram-v2
[*] --> idle
idle --> submitting: SUBMIT [isValid]
submitting --> submitted: onDone
submitting --> failed: onError
failed --> submitting: RETRY
failed --> idle: DISMISS
submitted --> [*]
```
- One node per finite state; nested states become `state parent { ... }`; parallel regions are separated by `--`.
- Label every edge with its event, and append `[guard]` where a guard gates it.
- Show `onDone`/`onError` for invoked actors — the error path is the half reviewers most need to see.
- Keep it to one flow. A diagram spanning an actor system is a system diagram, and belongs in the `diagrams` skill's care.
- Keep composite and concurrent boundaries legal, and keep parser-sensitive labels safe — an event name containing a colon, a guard containing a bracket, or a state name Mermaid reads as a keyword all need escaping or renaming in the label.
`diagrams` owns format choice, validation, and the accessible text explanation that must accompany any committed diagram. Stately Studio and `@statelyai/inspect` render the live machine and remain the better loop for interactive exploration — Mermaid loses that interactivity, which is the one real thing it costs — but a checked-in Mermaid render is what makes the machine reviewable in a diff.
## Anti-Patterns
**Under-modeling — the common failure, listed first because it is the one that actually happens:**
- A `submitting`/`saving`/`isLoading` flag living in component `useState` while a machine next to it already owns the command, its errors, and its retries — one lifecycle, two sources of truth.
- Classifying flow state as presentational because of how it renders — "it is just a disabled button", "it is just a spinner".
- Reading rung 1 or rung 3 of the ladder as permission to hand-roll, when the flow already has retry, cancellation, staleness, or an owning actor.
- A promise chain in an event handler doing the machine's job: set flag, `await`, clear flag, set error, clear error before the next attempt.
- `useEffect` chains sequencing steps by watching flags — transitions trying to escape from effects.
- Canvas, editor, permission, or command-dispatch interactions left in ad-hoc handlers because each individual handler looked small.
**Over-modeling — real, but rarer:**
- An 80-line machine wrapping a modal boolean — over-modeling trivial state (rung 1–2 of the ladder).
- Machine-as-reducer: one or two states, every event an unconditional `assign` — demote to `@xstate/store`.
**Modeling done wrong:**
- Boolean flags accumulating in context, gating behavior through guards — hidden finite states.
- Form field *values* mirrored into machine context on every keystroke — the machine owns the submission lifecycle, the form layer owns the current text. (Note the split: `values` stay local, `submitting` does not.)
- `sendParent` coupling children to parent shapes — pass the parent ref via `input` and use `sendTo`.
- Spawned actors never stopped; `stopChild` without clearing the context ref.
- Asserting transient `always` states, or component tests reading `actorRef.getSnapshot()`.
- v4 idioms in new code (`cond`, `services`, `interpret`, typegen) — translate before shipping.
- Inspector or `@statelyai/inspect` wired into production builds.
- A committed diagram left behind after the machine changed — a picture a reviewer will trust and the code no longer matches.
- Claiming a diagram renders without having checked it.
## Completion Check
- For every piece of state in the touched code, can you say what *changes* it — and does anything changed by a server, socket, timer, or another actor still sit in `useState`?
- Did you check where this repository allows machines to live before creating one, rather than after CI rejected it?
- If a machine already existed for this flow, does it now own the whole lifecycle, with no flag mirroring part of it in a component?
- Can you name the flow this machine owns, and would a diagram of it make sense to a non-developer?
- Is every impossible state actually unrepresentable — no contradictory booleans, no guard re-deriving a status?
- Does every effect with a result, lifetime, cancellation, or failure mode live in a named actor (with `onDone`/`onError` modeled where the actor's protocol has them), and is every fire-and-forget effect a named action?
- Does the machine suite cover every guard boundary, ignored event, error path, and timeout headlessly — and do component tests touch only the DOM?
- Are external events schema-validated before `send`, and is persisted-snapshot compatibility across releases either tested or explicitly not needed?
- Is everything v5 idiom — `setup()`, named implementations, `createActor` — with no v4 vocabulary?
- Does every designed or changed machine have a diagram regenerated from its final definition, with composite and concurrent boundaries legal, parser-sensitive labels safe, and the validation result reported accurately?
- Would the next reviewer learn the flow faster from the chart than from the diff? If not, the model is not carrying its weight yet.More Testing skills
tdd
mattpocock/skills
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
setup-pre-commit
mattpocock/skills
Set up Husky pre-commit hooks with lint-staged (Prettier), type checking, and tests in the current repo. Use when user wants to add pre-commit hooks, set up Husky, configure lint-staged, or add commit-time formatting/typechecking/testing.
agent-browser
vercel-labs/agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.

