xstate-v5
npx skills add https://github.com/programbo/xstate-skill --skill xstate-v5
Agent 安装分布
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.) whenstate.valuealready represents the mode. - If a temporary migration flag exists, document rationale and removal plan.
- Do not duplicate mode in context booleans (
- XState v5 strictness:
- Use
setup({...}).createMachine({...}). - Implement
actions/guards/actorsinsetup(...). - Send event objects only.
- Avoid forbidden v4 patterns (
interpret,Machine,cond,send,pure,choose, etc.).
- Use
- Enforce explicit async and actor boundaries:
- Request/response:
invokewithonDoneandonError. - Long-lived collaborators:
spawnChild/stopChildand explicit routing viasendTo.
- Request/response:
Fail-closed rule:
- If any detail is ambiguous, load
references/xstate-v5-rules.mdbefore 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, andactorsin thesetup({...})object. - Prefer passing event-derived data via typed
paramsto 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.) whenstate.valuealready represents that mode.
Rules Table Of Contents
- Statechart Design: 0 Statechart Design (Before You Code)
- Core Principles: 1 Machine Creation Pattern, 2 Type Safety First
- Setup Object Rules: 3 Implementation Placement, 4 Parameter Typing
- Event Handling Rules: 5 Event Type Safety with assertEvent, 6 Event Object Requirement
- Deprecated Pattern Prevention: 7 Forbidden v4 Patterns, 8 Modern v5 Equivalents
- Context and Input Rules: 9 Context Initialization, 10 Context Updates
- Invoke and Actor Rules: 11 Invoke Configuration, 12 Actor Spawning
- Type Helper Rules: 13 Type Helpers
- Testing Rules: 14 Type-Safe Testing (14.1 Deterministic Actor Tests, 14.2 Model-Based Testing, 14.3 Graph Utilities)
- XState React Rules: 15 Shared State via createActorContext
- Best Practices Summary: 16 Code Organization, 17 Performance Considerations, 18 Error Handling, 19 Documentation
- Enforcement Rules for AI Agents: 20 Mandatory Patterns, 21 Quality Assurance, 22 When Docs Are Ambiguous
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)
- Read
references/xstate-v5-rules.mdfirst. - Identify the target surface area:
- Plain machine/actor (non-React)
- React integration (
createActorContext) or localuseActor - Migration from v4 patterns
- 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.
- See
- Design types first:
types.context,types.events, andtypes.input(andtypes.outputif applicable)- Use
zodschemas only if the codebase already uses Zod or the user asks for it.
- Put implementations in
setup({ actions, guards, actors }):- Actions/guards take typed
paramswhen they need event-derived data. - Only read event-specific fields inside implementations when necessary; use
assertEventthen.
- Actions/guards take typed
- Choose async boundaries:
- Prefer
invokewith typedinputfor request/response flows. - Use
spawnChild/stopChildfor long-lived child actors.
- Prefer
- Use v5 runtime APIs:
createActor(machine)(notinterpret)raise/sendTo(notsendaction)enqueueActions(notpure/choose)
- Validate:
tscpasses; noanyleaks in params.- No string event sends.
- No v4 forbidden imports or config keys (
cond,withContext,withConfig, etc.). - Prefer
waitForfor async actor tests andxstate/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/guardsread event fields only viaparamsorassertEvent.- No
interpret,Machine,withConfig,withContext,cond,send,pure,choose. invokehasonError(andonDonewhen appropriate).- React usage prefers
createActorContextat module scope; selectors are stable and useshallowEqualfor objects.
Common Fixes
-
Event payload used inside an action:
- Prefer: compute payload in
paramsand make the action implementation depend onparams. - Otherwise: add
assertEvent(event, 'someType')inside the implementation.
- Prefer: compute payload in
-
v4
cond:- Rename to
guardand move guard implementation intosetup({ guards: { ... } }).
- Rename to
-
v4
interpret:- Replace with
createActor(machine)and.start()where needed.
- Replace with
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.