xstate-v5

📁 programbo/xstate-skill 📅 1 day ago
1
总安装量
2
周安装量
#50097
全站排名
安装命令
npx skills add https://github.com/programbo/xstate-skill --skill xstate-v5

Agent 安装分布

opencode 2
codex 2
gemini-cli 2
kilo 1
trae 1

Skill 文档

xstate-v5 (Strict) Skill

Build, refactor, and review XState v5 state machines (TypeScript and React) using a strict, strongly-typed ruleset.

Source of truth: references/xstate-v5-rules.md (this repo, resolved from the skill root). If anything in this skill conflicts with the rules file, follow the rules file.

Inlined Non-Negotiable Contract (Agent Must Follow)

These requirements are intentionally duplicated here so an agent can comply without first loading external references:

  • Design first, code second:
    • Produce planning artifacts before implementation (state inventory, event catalog, transition table, async/actor map, acceptance-test scenarios).
    • Include a boundary/decomposition decision record (what is split vs orchestrated, and why).
  • No god-machine architecture:
    • Unrelated domains (auth, toasts, navigation, workflows, transport/retry policy) must not be fully modeled in one machine.
    • A single machine is acceptable only as a thin orchestration root or tightly coupled app-shell parallel regions.
  • No state-mirroring context flags:
    • Do not duplicate mode in context booleans (isLoading, isAuthenticated, etc.) when state.value already represents the mode.
    • If a temporary migration flag exists, document rationale and removal plan.
  • XState v5 strictness:
    • Use setup({...}).createMachine({...}).
    • Implement actions/guards/actors in setup(...).
    • Send event objects only.
    • Avoid forbidden v4 patterns (interpret, Machine, cond, send, pure, choose, etc.).
  • Enforce explicit async and actor boundaries:
    • Request/response: invoke with onDone and onError.
    • Long-lived collaborators: spawnChild/stopChild and explicit routing via sendTo.

Fail-closed rule:

  • If any detail is ambiguous, load references/xstate-v5-rules.md before writing or reviewing machine code.
  • If that file is missing/unreadable, stop and report the issue instead of guessing.

When To Use

Use this skill when the task involves:

  • XState v5 machines/actors (xstate, @xstate/react)
  • Refactoring XState code to be more type-safe and idiomatic v5
  • Migrating XState v4 patterns (Machine/interpret/cond/send/pure/choose/etc.) to v5 equivalents
  • Planning/designing statecharts prior to implementing them as XState v5 machines
  • Designing actor boundaries (invoke vs spawnChild) and React integration via createActorContext

Hard Requirements (Enforced)

  • Prefer setup({...}).createMachine({...}) for all machines.
  • Implement all actions, guards, and actors in the setup({...}) object.
  • Prefer passing event-derived data via typed params to actions/guards.
  • Never send string events. Always send event objects: actor.send({ type: '...' }).
  • Forbid XState v4 legacy APIs and patterns listed in references/xstate-v5-rules.md.
  • Do not model unrelated domains (auth, notifications/toasts, navigation, data lifecycle, etc.) inside one machine, except a thin orchestration machine.
  • Do not mirror state in context booleans (isLoading, isAuthenticated, etc.) when state.value already represents that mode.

Rules Table Of Contents

Quick Start

Minimal typed machine skeleton:

import { setup, assign } from "xstate"

type Ctx = { count: number }

type Ev = { type: "inc" } | { type: "add"; amount: number } | { type: "reset" }

type Input = { initialCount?: number }

export const counterMachine = setup({
  types: {
    context: {} as Ctx,
    events: {} as Ev,
    input: {} as Input,
  },
  actions: {
    inc: assign({ count: ({ context }) => context.count + 1 }),
    add: assign({
      count: ({ context }, params: { amount: number }) => context.count + params.amount,
    }),
    reset: assign({ count: ({ input }) => input.initialCount ?? 0 }),
  },
}).createMachine({
  id: "counter",
  context: ({ input }) => ({ count: input.initialCount ?? 0 }),
  on: {
    inc: { actions: "inc" },
    add: {
      actions: {
        type: "add",
        params: ({ event }) => ({ amount: event.amount }),
      },
    },
    reset: { actions: "reset" },
  },
})

React shared state skeleton:

import { createActorContext, shallowEqual } from "@xstate/react"
import { type SnapshotFrom } from "xstate"
import { counterMachine } from "./counterMachine"

const CounterCtx = createActorContext(counterMachine)

export const CounterProvider = CounterCtx.Provider
export const useCounterSelector = CounterCtx.useSelector
export const useCounterActorRef = CounterCtx.useActorRef

const selectCount = (s: SnapshotFrom<typeof counterMachine>) => s.context.count
export const useCount = () => useCounterSelector(selectCount)

const selectCtx = (s: SnapshotFrom<typeof counterMachine>) => s.context
export const useCounterContext = () => useCounterSelector(selectCtx, shallowEqual)

Workflow (What To Do Each Time)

  1. Read references/xstate-v5-rules.md first.
  2. Identify the target surface area:
    • Plain machine/actor (non-React)
    • React integration (createActorContext) or local useActor
    • Migration from v4 patterns
  3. Design the statechart first:
    • See references/xstate-v5-rules.md#0-statechart-design-before-you-code.
    • Write down: state inventory, event catalog (payload + source), transition table, async/actor boundaries, and acceptance-test scenarios.
    • Record a boundary/decomposition decision before type design:
      • Which concerns belong in separate actors/machines (based on lifecycle, owner, and failure mode).
      • Which concerns are intentionally orchestrated at top-level (and why).
      • Any exception note if keeping logic in one machine.
    • Default: do this unless the user explicitly asks to skip planning.
  4. Design types first:
    • types.context, types.events, and types.input (and types.output if applicable)
    • Use zod schemas only if the codebase already uses Zod or the user asks for it.
  5. Put implementations in setup({ actions, guards, actors }):
    • Actions/guards take typed params when they need event-derived data.
    • Only read event-specific fields inside implementations when necessary; use assertEvent then.
  6. Choose async boundaries:
    • Prefer invoke with typed input for request/response flows.
    • Use spawnChild/stopChild for long-lived child actors.
  7. Use v5 runtime APIs:
    • createActor(machine) (not interpret)
    • raise / sendTo (not send action)
    • enqueueActions (not pure/choose)
  8. Validate:
    • tsc passes; no any leaks in params.
    • No string event sends.
    • No v4 forbidden imports or config keys (cond, withContext, withConfig, etc.).
    • Prefer waitFor for async actor tests and xstate/graph (createTestModel, getShortestPaths, etc.) for model/graph-driven test generation.

When A Monolith Is Acceptable

Use a single top-level machine only when it is a thin orchestration root that coordinates child actors or tightly coupled parallel app-shell concerns. Even then, unrelated domain logic should live in separate actors/machines and communicate via explicit events.

Review Checklist (Use When Auditing PRs)

  • All machines are setup(...).createMachine(...).
  • Machine has a clear statechart plan (states/events/transition table) or the PR description includes it.
  • No domain-smell: one machine has one responsibility or explicit orchestration scope.
  • No state-mirroring context flags without documented rationale.
  • No implementations inside types.
  • actions/guards read event fields only via params or assertEvent.
  • No interpret, Machine, withConfig, withContext, cond, send, pure, choose.
  • invoke has onError (and onDone when appropriate).
  • React usage prefers createActorContext at module scope; selectors are stable and use shallowEqual for objects.

Common Fixes

  • Event payload used inside an action:

    • Prefer: compute payload in params and make the action implementation depend on params.
    • Otherwise: add assertEvent(event, 'someType') inside the implementation.
  • v4 cond:

    • Rename to guard and move guard implementation into setup({ guards: { ... } }).
  • v4 interpret:

    • Replace with createActor(machine) and .start() where needed.

Notes

If official docs feel ambiguous, follow the rules file guidance: confirm behavior against XState v5 source, issues/discussions, or a minimal reproduction/type-test rather than guessing.