xState React
Your Role
You are an expert in xState v5 actor-based state management with React and TypeScript. You understand state machines, statecharts, the actor model, and React integration patterns for managing complex application logic.
Overview
xState is an actor-based state management and orchestration solution for JavaScript and TypeScript applications. It uses event-driven programming, state machines, statecharts, and the actor model to handle complex logic in predictable, robust, and visual ways.
When to use xState:
-
Complex UI flows (multi-step forms, wizards, checkout processes)
-
State with many transitions and edge cases
-
Logic that needs to be visualized and validated
-
Processes with async operations and error handling
-
State that can be in multiple "modes" (loading, error, success, idle)
When to use simpler state instead (useState, Zustand):
-
Simple UI toggles and counters
-
Form state confined to a single component
-
State without complex transition logic
-
CRUD operations with straightforward loading states
Quick Start
Create a state machine and use it in a React component:
"use client";
import { createMachine } from "xstate"; import { useMachine } from "@xstate/react";
const toggleMachine = createMachine({ id: "toggle", initial: "inactive", states: { inactive: { on: { TOGGLE: "active" } }, active: { on: { TOGGLE: "inactive" } } } });
function Toggle() { const [state, send] = useMachine(toggleMachine);
return ( <button onClick={() => send({ type: "TOGGLE" })}> {state.value === "inactive" ? "Off" : "On"} </button> ); }
React Hooks API
useMachine
Create and run a machine within a component's lifecycle:
import { useMachine } from "@xstate/react"; import { someMachine } from "./machines/someMachine";
function Component() { const [state, send, actorRef] = useMachine(someMachine, { input: { userId: "123" } // Optional input });
return ( <div> <p>Current state: {JSON.stringify(state.value)}</p> <p>Context: {JSON.stringify(state.context)}</p> <button onClick={() => send({ type: "SOME_EVENT" })}>Send</button> </div> ); }
useActor
Subscribe to an existing actor (created outside the component):
import { createActor } from "xstate"; import { useActor } from "@xstate/react"; import { todoMachine } from "./machines/todoMachine";
// Create actor outside component (e.g., in a module or context) const todoActor = createActor(todoMachine); todoActor.start();
function TodoApp() { const [state, send] = useActor(todoActor);
return ( <ul> {state.context.todos.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); }
useSelector
Optimize re-renders by selecting specific state:
import { useSelector } from "@xstate/react";
function TodoCount({ actorRef }) { // Only re-renders when todos.length changes const count = useSelector(actorRef, (state) => state.context.todos.length);
return <span>{count} todos</span>; }
function IsLoading({ actorRef }) { // Only re-renders when loading state changes const isLoading = useSelector(actorRef, (state) => state.matches("loading"));
return isLoading ? <Spinner /> : null; }
TypeScript Patterns
Typing Machines with types
Define context and events using the types property:
import { createMachine, assign } from "xstate";
type FormContext = { name: string; email: string; errors: string[]; };
type FormEvent = | { type: "UPDATE_NAME"; value: string } | { type: "UPDATE_EMAIL"; value: string } | { type: "SUBMIT" } | { type: "RESET" };
const formMachine = createMachine({ types: {} as { context: FormContext; events: FormEvent; }, id: "form", initial: "editing", context: { name: "", email: "", errors: [] }, states: { editing: { on: { UPDATE_NAME: { actions: assign({ name: ({ event }) => event.value }) }, UPDATE_EMAIL: { actions: assign({ email: ({ event }) => event.value }) }, SUBMIT: "submitting" } }, submitting: { // ... } } });
Using setup() for Reusable Definitions
Define actions, guards, actors, and delays in a type-safe way:
import { setup, assign } from "xstate";
type AuthContext = { userId: string | null; retries: number; };
type AuthEvent = | { type: "LOGIN"; username: string; password: string } | { type: "LOGOUT" } | { type: "SUCCESS"; userId: string } | { type: "FAILURE" };
const authMachine = setup({ types: {} as { context: AuthContext; events: AuthEvent; }, actions: { setUser: assign({ userId: ({ event }) => (event as { userId: string }).userId }), clearUser: assign({ userId: null, retries: 0 }), incrementRetries: assign({ retries: ({ context }) => context.retries + 1 }) }, guards: { hasReachedMaxRetries: ({ context }) => context.retries >= 3, isAuthenticated: ({ context }) => context.userId !== null } }).createMachine({ id: "auth", initial: "loggedOut", context: { userId: null, retries: 0 }, states: { loggedOut: { on: { LOGIN: "authenticating" } }, authenticating: { on: { SUCCESS: { target: "loggedIn", actions: "setUser" }, FAILURE: [ { guard: "hasReachedMaxRetries", target: "loggedOut", actions: "clearUser" }, { actions: "incrementRetries" } ] } }, loggedIn: { on: { LOGOUT: { target: "loggedOut", actions: "clearUser" } } } } });
Core Concepts
States and Transitions
States represent the possible modes of your system. Transitions define how events move between states:
const machine = createMachine({ initial: "idle", states: { idle: { on: { FETCH: "loading" } }, loading: { on: { SUCCESS: "success", ERROR: "error" } }, success: { type: "final" }, error: { on: { RETRY: "loading" } } } });
Context
Context holds extended state data:
const machine = createMachine({ context: { count: 0, user: null }, // ... });
// Access in component const count = state.context.count;
Actions
Actions are fire-and-forget side effects:
import { assign } from "xstate";
const machine = createMachine({ // ... states: { active: { entry: assign({ count: ({ context }) => context.count + 1 }), exit: () => console.log("Leaving active state") } } });
Best Practices
DO
-
Use setup() for type-safe action and guard definitions
-
Use useSelector for optimized state selection
-
Define explicit types for context and events
-
Use state.matches() for hierarchical state checking
-
Keep machines pure and side-effect-free (use actions for effects)
DON'T
-
Don't store the entire snapshot object in React state
-
Don't mutate context directly (use assign )
-
Don't use xState for simple toggle/counter state
-
Don't forget to handle all possible states in your UI
Client Component Requirement
xState hooks must be used in Client Components. Add the "use client" directive:
"use client";
import { useMachine } from "@xstate/react";
Additional Documentation
For comprehensive xState documentation including advanced patterns, use the Context7 MCP:
Use Context7 MCP with library ID "/statelyai/xstate" to fetch:
- Detailed invoke/spawn patterns
- Parallel and history states
- Actor communication
- Testing strategies
See ./references/patterns.md for common patterns including async operations, guards, parallel states, and persistence.
Quick Reference
Task Pattern
Create machine createMachine({ id, initial, states, context })
Use in component const [state, send] = useMachine(machine)
Check state state.matches("loading") or state.value
Send event send({ type: "EVENT_NAME", ...payload })
Read context state.context.someValue
Update context assign({ key: ({ context, event }) => newValue })
Conditional guard: "guardName" or guard: ({ context }) => …
Async operation invoke: { src: fromPromise(...), onDone, onError }
Optimized select useSelector(actorRef, (state) => state.context.x)