Claude
Skills
Sign in
Back

xstate-helper

Included with Lifetime
$97 forever

XState v5 state machines, statecharts, and the actor model for complex, event-driven application logic. When user works with XState, state machines, statecharts, actors, createMachine/setup, createActor, @xstate/react, @xstate/store, useMachine/useActor/useSelector, invoke/spawn, guards/actions/assign, or needs to model complex async flows, wizards, and event-driven state.

Web Dev

What this skill does


# XState Helper Agent

Guidance for **XState v5** — state machines, statecharts, and actors. This monorepo uses Bun and a
strict ESLint config; read the **Monorepo Gotchas** section before writing any machine here — the
documented `setup({ types })` idiom from the official docs **does not lint-pass in this repo** and
there is a specific workaround.

## Versions (verified mid-2026)

| Package | Latest | Notes |
|---|---|---|
| `xstate` | 5.32.0 | Core. In-repo use: `discord-plays-pokemon/.../backend` pins `^5.32.0` |
| `@xstate/react` | 6.1.0 | React hooks. peer: `react 16.8–19`, `xstate ^5.28` |
| `@xstate/store` | 4.1.0 | Lightweight store (Zustand-like). React binding is a **separate** package |
| `@xstate/store-react` | 2.0.0 | React binding for store v4 — `@xstate/store/react` was **removed** in v4 |

```bash
bun add xstate                 # core
bun add @xstate/react          # React bindings
bun add @xstate/store @xstate/store-react   # lightweight store + its React binding
```

Requires **TypeScript 5.0+**. In `tsconfig.json` set `"strictNullChecks": true` (required — types
break without it) and `"skipLibCheck": true` (recommended).

## What's New in XState v5 (2024–2026)

v5 made **actors first-class**, removed v4 magic (typegen, implicit string events, external-by-default
transitions), and added `setup()` for type safety. Recent 5.x minors:

- **`actor.select(selector, eq?)`** (5.29) — derive a framework-agnostic `Readable<T>` off any actor's
  snapshot; `.get()` + `.subscribe()` (only fires when the selected value changes). No hook needed.
- **Routable states** (5.28) — a state with `route: {}` + explicit `id` is reachable from anywhere via
  `{ type: 'xstate.route', to: '#id' }`. Routes accept a `guard` (string ref resolved since 5.31.1).
- **`getMicrosteps()` / `getInitialMicrosteps()`** (5.27) — pure `[snapshot, actions]` tuples per
  microstep, so you can inspect every intermediate (incl. `always`) state without executing actions.
- **`maxIterations`** (5.31) — infinite-loop guard for eventless transitions (default `Infinity`).
- **`filterEvents`** (5.30) — `xstate/graph` + `createTestModel` traversal limited to currently-enabled
  events (e.g. `state.can(event)`).
- **`setup().assign/raise/sendTo/emit/spawnChild/enqueueActions(...)`** (5.22) — type-bound action
  helpers; no inline generics. Current best practice for typed `enqueueActions`/`emit`/`spawnChild`.
- **`setup.extend()`** (5.24) and **`setup().createStateConfig()`** (5.21) — composable, modular setups.
- **`mapState(snapshot, mapper)`** (5.31), **`getNextTransitions(snapshot)`** (5.26), partial descriptors
  in `assertEvent(event, 'FEEDBACK.*')` (5.25).
- **Model-based testing moved into core** — import from `xstate/graph` (`@xstate/test` is deprecated).
- **`@xstate/store` v3/v4** — `store.trigger.someEvent(...)` typed API, `.with(persist({ name }))`,
  atoms, schema validation; v4 split framework bindings into `@xstate/store-react` etc.

## Core Mental Model

> **Everything is an actor.** An actor is a live entity with private state, a mailbox (one event at a
> time), and async message passing. A state machine is the most robust way to describe an actor's
> behavior — but `fromPromise`, `fromCallback`, `fromObservable`, and `fromTransition` are actor logic too.

- **Logic** (the machine/definition) is inert. `createActor(logic)` makes a **running actor**.
- `actor.send({ type })` — events are **objects**, never bare strings (v4 allowed strings; v5 does not).
- `actor.getSnapshot()` reads current state; `actor.subscribe(fn)` observes changes (does **not** emit
  the current value immediately — read `getSnapshot()` for that).

```ts
import { createMachine, createActor } from "xstate";

const toggleMachine = createMachine({
  id: "toggle",
  initial: "inactive",
  states: {
    inactive: { on: { toggle: { target: "active" } } },
    active: { on: { toggle: { target: "inactive" } } },
  },
});

const actor = createActor(toggleMachine);
actor.subscribe((snapshot) => console.log(snapshot.value));
actor.start();            // logs "inactive"
actor.send({ type: "toggle" }); // logs "active"
actor.stop();
```

### Machine vs Store — pick the right tool

- **State machine (`xstate`)** — many discrete states, guarded transitions, hierarchy/parallelism, async
  orchestration. Use when *the transitions are the hard part* (wizards, checkout, auth, media players).
- **Store (`@xstate/store`)** — a small, event-based shared-state container (Zustand-like) when you do
  **not** need statecharts. See `references/xstate-store.md`.
- **Not XState at all** — for a bag of independent values prefer Zustand/Jotai; for server cache use
  TanStack Query. A localized form/wizard reducer can just be `useReducer`. XState earns its weight when
  illegal states and complex transitions are the actual problem (it makes impossible states
  unrepresentable). Don't reach for it to hold three booleans.

## setup() + createMachine — the typed entry point

`setup({ types, actors, guards, actions, delays })` registers named implementations and types, then
`.createMachine(config)` references them by string. This is the v5 idiom (v5 has **no typegen**).

```ts
import { setup, assign, fromPromise } from "xstate";

// ⚠️ In THIS repo, do NOT inline `context: {} as Ctx` — see Monorepo Gotchas below.
const machine = setup({
  types: {
    context: {} as { count: number; user: string | undefined },
    events: {} as { type: "inc"; by: number } | { type: "reset" } | { type: "LOAD" },
    input: {} as { start: number },
  },
  actors: {
    loadUser: fromPromise(async ({ input }: { input: { id: number } }) => {
      const res = await fetch(`/api/users/${input.id}`);
      return res.json();
    }),
  },
  guards: {
    canInc: ({ context }) => context.count < 100,
  },
  actions: {
    increment: assign({ count: ({ context, event }) => {
      // narrow the event to access its payload safely
      return event.type === "inc" ? context.count + event.by : context.count;
    } }),
  },
}).createMachine({
  id: "counter",
  initial: "idle",
  context: ({ input }) => ({ count: input.start, user: undefined }),
  states: {
    idle: {
      on: {
        inc: { guard: "canInc", actions: "increment" },
        reset: { actions: assign({ count: 0 }) },
        LOAD: { target: "loading" },
      },
    },
    loading: {
      invoke: {
        src: "loadUser",
        input: { id: 1 },
        onDone: { target: "idle", actions: assign({ user: ({ event }) => event.output.name }) },
        onError: { target: "idle" },
      },
    },
  },
});

const actor = createActor(machine, { input: { start: 0 } }).start();
actor.send({ type: "inc", by: 5 });
```

Override implementations per instance with `machine.provide({ actions, actors, guards, delays })`
(replaces v4 `withConfig`). See `references/actors-and-machines.md` for the full machine/actor surface
(hierarchical & parallel states, history, `after`, `always`, `raise`, `enqueueActions`, `emit`, spawn).

## Context, Actions & Guards (quick reference)

```ts
import { assign, raise, sendTo, enqueueActions, and, or, not, stateIn, assertEvent } from "xstate";

// assign — object or function form; context is immutable, only assign mutates it
assign({ count: ({ context }) => context.count + 1 });
assign(({ context, event }) => ({ count: context.count + 1 }));

// raise — send an event to SELF (optionally delayed)
raise({ type: "RETRY" }, { delay: 1000 });

// sendTo — send to another actor by id, ref, or resolver
sendTo("childId", { type: "PING" });
sendTo(({ context }) => context.someRef, { type: "PING" });

// enqueueActions — replaces v4 choose/pure; imperative composition of actions
enqueueActions(({ context, enqueue, check }) => {
  enqueue.assign({ count: context.count + 1 });
  if (check({ type: "someGuard" })) enqueue("namedAction");
  enqueue.sendTo("childId", { type: "GO" });
});

// guards — string ref, params, or higher-order combinators
guard: and(["isValid", or(["isAdmin", 

Related in Web Dev