Skip to content

Basic State Machine

Learn how to define a simple toggle machine using the current XState v5 + XMachines setup().createMachine() + definePlayer() pattern.

Use Case

This example demonstrates a toggle switch — the foundation for understanding state machines in XMachines. It is the simplest possible machine: two states, one event type, no router, no catalog.

Complete Code

import { setup } from "xstate";
import { definePlayer } from "@xmachines/play-xstate";
// 1. Declare types for context, events, and input via setup()
const toggleSetup = setup({
types: {
context: {} as { toggleCount: number },
events: {} as { type: "toggle" },
input: {} as { toggleCount?: number } | undefined,
},
});
// 2. Create the machine using setup().createMachine()
const toggleMachine = toggleSetup.createMachine({
id: "toggle",
initial: "off",
context: ({ input }) => ({
toggleCount: input?.toggleCount ?? 0,
}),
states: {
off: {
on: {
toggle: {
target: "on",
actions: toggleSetup.assign({
toggleCount: ({ context }) => context.toggleCount + 1,
}),
},
},
},
on: {
on: {
toggle: {
target: "off",
actions: toggleSetup.assign({
toggleCount: ({ context }) => context.toggleCount + 1,
}),
},
},
},
},
});
// 3. Create a player factory
const createPlayer = definePlayer({ machine: toggleMachine });
// 4. Create and start an actor
const actor = createPlayer();
actor.start();
// 5. Read initial state via getSnapshot()
console.log(actor.getSnapshot().value); // "off"
console.log(actor.getSnapshot().context.toggleCount); // 0
// 6. Read state via TC39 Signal
console.log(actor.state.get().value); // "off"
// 7. Send events
actor.send({ type: "toggle" });
console.log(actor.getSnapshot().value); // "on"
console.log(actor.getSnapshot().context.toggleCount); // 1
actor.send({ type: "toggle" });
console.log(actor.getSnapshot().value); // "off"
console.log(actor.getSnapshot().context.toggleCount); // 2
// 8. Cleanup
actor.stop();

Code Explanation

  1. setup({ types }) — Declares TypeScript types for context, events, and input before the machine is created. This is the XState v5 typed entry point. Never pass type parameters directly to createMachine.

  2. toggleSetup.createMachine(...) — Creates the state machine. Using the scoped toggleSetup.createMachine (instead of the bare createMachine from xstate) ensures that guards and actions are type-checked against the declared types.

  3. toggleSetup.assign(...) — Assigns context mutations using the typed setup’s assign helper. This provides full inference from the declared context type — no manual type assertions needed.

  4. definePlayer({ machine }) — Wraps the machine in a factory. Calling createPlayer() returns a PlayerActor — XMachines’ actor type that extends XState’s actor with TC39 Signal reactivity and Play Architecture hooks.

  5. actor.start() — Activates the machine. Always call start() before sending events.

  6. actor.getSnapshot() — Returns the current XState snapshot synchronously. .value is the state name; .context is the typed context object.

  7. actor.state.get() — TC39 Signal access to the current snapshot. Useful when wiring infrastructure that observes signals (renderers, router bridges). Equivalent to actor.getSnapshot() for one-time reads.

  8. Event types use dot-separated lowercase"toggle" not "TOGGLE". This matches the XMachines convention used throughout the codebase (e.g. "auth.login", "auth.logout", "play.route").

Key Concepts

  • setup({ types }): The XState v5 typed entry point. Declares context, event, and input shapes before machine creation.
  • definePlayer({ machine }): Creates a factory function that returns PlayerActor instances.
  • actor.state: A Signal.State<Snapshot> — TC39 Signals-based reactive handle on the machine snapshot.
  • actor.getSnapshot(): Synchronous snapshot access — use for one-time reads and TypeScript-typed context inspection.
  • Typed assign: Always call setup.assign(...) (not the bare assign from xstate) for context mutations.

Next Steps