state-machines
SKILL.md
State Machines with XState v5
When to Use State Machines
Use state machines for complex state that has:
- Multiple distinct states (idle, loading, error, success).
- Guarded transitions (can only submit if form is valid).
- Sequences (multi-step wizards, onboarding flows).
- Parallel concerns (loading data + playing animation simultaneously).
Don't use for simple toggles or value-tracking — useState is fine for those.
Setup
npm install xstate @xstate/react
Requires TypeScript 5.0+ and strictNullChecks: true.
Basic Machine
import { setup, assign } from "xstate";
const toggleMachine = setup({
types: {
context: {} as { count: number },
events: {} as { type: "TOGGLE" } | { type: "RESET" },
},
}).createMachine({
id: "toggle",
initial: "inactive",
context: { count: 0 },
states: {
inactive: {
on: {
TOGGLE: {
target: "active",
actions: assign({ count: ({ context }) => context.count + 1 }),
},
},
},
active: {
on: {
TOGGLE: { target: "inactive" },
RESET: {
target: "inactive",
actions: assign({ count: 0 }),
},
},
},
},
});
React Integration
useMachine
import { useMachine } from "@xstate/react";
function Toggle() {
const [state, send] = useMachine(toggleMachine);
return (
<div>
<p>State: {state.value}</p>
<p>Count: {state.context.count}</p>
<button onClick={() => send({ type: "TOGGLE" })}>Toggle</button>
{state.matches("active") && <button onClick={() => send({ type: "RESET" })}>Reset</button>}
</div>
);
}
useActor with Global Actors
import { createActor } from "xstate";
export const toggleActor = createActor(toggleMachine).start();
import { useSelector } from "@xstate/react";
import { toggleActor } from "./machines/toggle";
function ToggleStatus() {
const count = useSelector(toggleActor, (state) => state.context.count);
const isActive = useSelector(toggleActor, (state) => state.matches("active"));
return (
<p>
{isActive ? "Active" : "Inactive"} ({count})
</p>
);
}
Use useSelector for performance — it only re-renders when the selected value changes.
Fetch Machine Pattern
const fetchMachine = setup({
types: {
context: {} as {
data: unknown | null;
error: string | null;
},
events: {} as { type: "FETCH"; url: string } | { type: "RETRY" },
input: {} as { url: string },
},
actors: {
fetchData: fromPromise(async ({ input }: { input: { url: string } }) => {
const response = await fetch(input.url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}),
},
}).createMachine({
id: "fetch",
initial: "idle",
context: { data: null, error: null },
states: {
idle: {
on: { FETCH: "loading" },
},
loading: {
invoke: {
src: "fetchData",
input: ({ event }) => ({ url: event.url }),
onDone: {
target: "success",
actions: assign({ data: ({ event }) => event.output }),
},
onError: {
target: "failure",
actions: assign({ error: ({ event }) => event.error.message }),
},
},
},
success: {
on: { FETCH: "loading" },
},
failure: {
on: {
RETRY: "loading",
FETCH: "loading",
},
},
},
});
Guards
Conditional transitions:
const machine = setup({
types: {
context: {} as { attempts: number; maxAttempts: number },
events: {} as { type: "RETRY" },
},
guards: {
canRetry: ({ context }) => context.attempts < context.maxAttempts,
hasExceededMax: ({ context }) => context.attempts >= context.maxAttempts,
},
}).createMachine({
// ...
states: {
failure: {
on: {
RETRY: [
{ target: "loading", guard: "canRetry" },
{ target: "exhausted", guard: "hasExceededMax" },
],
},
},
},
});
Actions
Side effects on transitions:
const machine = setup({
types: {
context: {} as { notifications: string[] },
events: {} as { type: "SUBMIT"; data: FormData },
},
actions: {
showToast: ({ context, event }) => {
toast.success("Submitted successfully");
},
logEvent: ({ context, event }) => {
analytics.track("form_submitted");
},
clearForm: assign({ notifications: [] }),
},
}).createMachine({
// ...
states: {
submitting: {
invoke: {
src: "submitForm",
onDone: {
target: "success",
actions: ["showToast", "logEvent", "clearForm"],
},
},
},
},
});
Nested (Hierarchical) States
Group related states:
states: {
authenticated: {
initial: "idle",
states: {
idle: {
on: { LOAD_PROFILE: "loadingProfile" },
},
loadingProfile: {
invoke: {
src: "fetchProfile",
onDone: "profileLoaded",
onError: "profileError",
},
},
profileLoaded: {},
profileError: {
on: { RETRY: "loadingProfile" },
},
},
on: {
LOGOUT: "#app.unauthenticated", // transition to top-level state
},
},
unauthenticated: {
on: { LOGIN: "authenticated" },
},
},
Parallel States
Multiple independent state regions active simultaneously:
type: "parallel",
states: {
upload: {
initial: "idle",
states: {
idle: { on: { START_UPLOAD: "uploading" } },
uploading: { on: { UPLOAD_DONE: "complete" } },
complete: {},
},
},
validation: {
initial: "idle",
states: {
idle: { on: { VALIDATE: "validating" } },
validating: { on: { VALID: "valid", INVALID: "invalid" } },
valid: {},
invalid: {},
},
},
},
Check parallel state with: state.matches({ upload: "complete", validation: "valid" }).
Multi-Step Form Example
const formMachine = setup({
types: {
context: {} as {
personalInfo: PersonalInfo | null;
address: Address | null;
payment: Payment | null;
},
events: {} as { type: "NEXT"; data: Record<string, unknown> } | { type: "BACK" } | { type: "SUBMIT" },
},
}).createMachine({
id: "checkout",
initial: "personalInfo",
context: { personalInfo: null, address: null, payment: null },
states: {
personalInfo: {
on: {
NEXT: {
target: "address",
actions: assign({ personalInfo: ({ event }) => event.data }),
},
},
},
address: {
on: {
NEXT: {
target: "payment",
actions: assign({ address: ({ event }) => event.data }),
},
BACK: "personalInfo",
},
},
payment: {
on: {
NEXT: {
target: "review",
actions: assign({ payment: ({ event }) => event.data }),
},
BACK: "address",
},
},
review: {
on: {
SUBMIT: "submitting",
BACK: "payment",
},
},
submitting: {
invoke: {
src: "submitOrder",
onDone: "success",
onError: "failure",
},
},
success: { type: "final" },
failure: {
on: { RETRY: "submitting", BACK: "review" },
},
},
});
Input (Machine Parameters)
Pass data to machines at creation time:
const machine = setup({
types: {
input: {} as { userId: string },
context: {} as { userId: string; user: User | null },
},
}).createMachine({
context: ({ input }) => ({
userId: input.userId,
user: null,
}),
// ...
});
// In React
const [state, send] = useMachine(machine, { input: { userId: "123" } });
Visualizing Machines
Use the Stately visual editor to design and debug machines:
npx stately viz
Or visit stately.ai/editor to paste your machine code and visualize the statechart.
Guidelines
- Model states explicitly — if you can name it (idle, loading, error, success), it's a state, not a boolean.
- Events describe what happened, not what should happen:
"FORM_SUBMITTED"not"SUBMIT_FORM". - Keep machines focused — one machine per concern. Compose with actors for complex systems.
- Use guards instead of if/else in actions — transitions are the decision points.
- Test machines without React — machines are pure logic and can be tested by sending events and asserting state.
Weekly Installs
2
Repository
grahamcrackers/skillsFirst Seen
14 days ago
Security Audits
Installed on
cline2
gemini-cli2
github-copilot2
codex2
kimi-cli2
cursor2