@xmachines/play-xstate
API / @xmachines/play-xstate
XState v5 adapter for the XMachines Play Architecture — bind state machines to the actor base with signal-driven reactivity and router integration.
Part of the XMachines Play monorepo.
Installation
npm install @xmachines/play-xstate xstatexstate ^5.31.0 is a peer dependency and must be installed alongside this package.
Quick Start
import { setup } from "xstate";import { definePlayer } from "@xmachines/play-xstate";
// 1. Define your XState v5 machineconst machine = setup({}).createMachine({ initial: "idle", states: { idle: { meta: { route: "/" } }, active: { meta: { route: "/active" } }, },});
// 2. Create a player factoryconst createPlayer = definePlayer({ machine });
// 3. Instantiate and start an actorconst actor = createPlayer();actor.start();
// 4. Observe TC39 Signal-based reactive stateconsole.log(actor.currentRoute.get()); // "/"console.log(actor.state.get().value); // "idle"
// 5. Send events — machine guards decide transitionsactor.send({ type: "activate" });
actor.stop();API Summary
definePlayer(config)
Creates a PlayerFactory from an XState v5 machine. The factory pattern enables multiple independent actor instances from a single configuration — useful for multi-user scenarios, SSR, or testing.
import { setup } from "xstate";import { definePlayer } from "@xmachines/play-xstate";
const machine = setup({ types: { context: {} as { userId: string }, input: {} as { userId: string }, },}).createMachine({ context: ({ input }) => ({ userId: input.userId }), initial: "home", states: { home: {} },});
const createPlayer = definePlayer({ machine, options: { onStart: (actor) => console.log("started"), onStop: (actor) => console.log("stopped"), onTransition: (actor, prev, next) => console.log("transitioned"), onStateChange: (actor, state) => console.log("state changed"), onError: (actor, err) => console.error(err), },});
// Each call returns an independent PlayerActor instanceconst alice = createPlayer({ userId: "alice" });const bob = createPlayer({ userId: "bob" });PlayerFactory signature
type PlayerFactory<TMachine> = ( input?: InputFrom<TMachine>, options?: PlayerFactoryResumeOptions<TMachine>,) => PlayerActor<TMachine>;Restoring from a snapshot
const snapshot = actor.getSnapshot();actor.stop();
// Restore to the exact saved stateconst restored = createPlayer({ userId: "alice" }, { snapshot });restored.start();console.log(restored.currentRoute.get()); // same route as when savedPlayerActor<TMachine>
Concrete actor class that wraps an XState v5 actor and exposes TC39 Signal-based reactive signals. Implements both Routable and Viewable interfaces from @xmachines/play-actor.
Signals
| Signal | Type | Description |
|---|---|---|
state | Signal.State<SnapshotFrom<TMachine>> | Current XState snapshot; updated on every active transition |
currentRoute | Signal.Computed<string | null> | Derived URL from active state’s meta.route template and context |
currentView | Signal.State<PlaySpec | null> | View spec from active state’s meta.view metadata; enriched with context params |
initialRoute | readonly string | null | Machine’s initial-state route (fixed at construction; used by router bridges for deep-link vs restore detection) |
Methods
| Method | Description |
|---|---|
start() | Start the actor and fire onStart hook |
stop() | Stop the actor, clean up subscriptions, fire onStop hook |
send(event) | Send a typed event to the machine; fires onTransition hook |
can(event) | Returns true if the current state can accept the given event |
getSnapshot() | Returns the current XState snapshot |
dispose() | Alias for stop() |
Signal usage example
import { Signal } from "@xmachines/play-signals";
const watcher = new Signal.subtle.Watcher(() => { queueMicrotask(() => { console.log("Route changed:", actor.currentRoute.get()); });});
watcher.watch(actor.currentRoute);actor.start();Guard utilities
Composable guard helpers that wrap XState’s built-in and(), or(), and not() for use in machine setup({ guards }) definitions.
import { setup } from "xstate";import { composeGuards, // AND logic: all guards must pass composeGuardsOr, // OR logic: at least one guard must pass negateGuard, // NOT logic: inverts a guard hasContext, // guard: context field is present and non-null eventMatches, // guard: event type matches a string contextFieldMatches, // guard: context field equals a value} from "@xmachines/play-xstate";
const machine = setup({ guards: { isLoggedIn: ({ context }) => !!context.userId, hasAdminRole: ({ context }) => context.role === "admin", },}).createMachine({ on: { accessAdmin: { guard: composeGuards(["isLoggedIn", "hasAdminRole"]), target: "adminPanel", }, accessDashboard: { guard: negateGuard("isGuest"), target: "dashboard", }, }, // ...});Routing utilities
Helper functions for declarative route configuration in XState machines.
formatPlayRouteTransitions(machineConfig)
Crawls machine states with meta.route and auto-generates play.route event handlers at the root level — eliminating boilerplate routing transitions.
import { setup } from "xstate";import { formatPlayRouteTransitions } from "@xmachines/play-xstate";
const config = formatPlayRouteTransitions({ id: "app", states: { home: { id: "home", meta: { route: "/home" }, }, profile: { id: "profile", meta: { route: "/users/:userId" }, }, },});
// config now includes auto-generated play.route handlers:// on: { "play.route": [ { target: ".home", guard: e => e.to === "#home" }, ... ] }const machine = setup({}).createMachine(config);Note: Every state with
meta.routemust also have an explicitidfield; omitting it throwsMissingStateIdErrorat machine-definition time.
Other routing exports
| Export | Description |
|---|---|
deriveRoute(meta) | Extract the route template string from a state’s metadata object |
isAbsoluteRoute(route) | Returns true if the route string is an absolute URL path |
buildRouteUrl(template, context) | Substitute :param placeholders in a route template using context values |
Exported Types
import type { PlayerConfig, // definePlayer() config argument shape PlayerOptions, // Lifecycle hooks (onStart, onStop, onTransition, onStateChange, onError) PlayerFactory, // Factory function returned by definePlayer() PlayerFactoryResumeOptions, // { snapshot? } for restoring actor state Guard, // Single XState guard predicate GuardArray, // Array of guards for compose helpers ComposedGuard, // Return type of composeGuards / composeGuardsOr / negateGuard RouteMachineConfig, // Minimal machine config accepted by formatPlayRouteTransitions RouteStateNode, // Single state node shape used during route crawling RouteContext, // Context shape expected by buildRouteUrl ({ params?, query?, basePath?, hash? }) RouteObject, // Route metadata object shape: { path: string } RouteMetadata, // Union: string | RouteObject} from "@xmachines/play-xstate";Error Classes
Error classes are exported from the @xmachines/play-xstate/errors sub-path to keep the main bundle lean.
import { MissingRouteParamError, // Required :param absent from context when resolving currentRoute MissingQueryContextError, // context.params present but context.query missing MissingStateIdError, // meta.route declared without a state id field InvalidMachineError, // PlayerActor constructed with a non-object machine InvalidEventError, // actor.send() called with null/undefined/non-object InvalidRouteMetadataError, // meta.route is neither a string nor { path: string } EmptyGuardArrayError, // composeGuards/composeGuardsOr called with empty array} from "@xmachines/play-xstate/errors";All error classes extend PlayError from @xmachines/play and carry typed detail fields (param, template, combinator, etc.) for programmatic inspection without message parsing.
Testing
# Run tests for this package in isolationnpm test -w packages/play-xstate
# Watch modenpm run test:watch -w packages/play-xstateTests use Vitest and live in packages/play-xstate/test/.
License
MIT — see LICENSE for details.
@xmachines/play-xstate - XState v5 adapter for Play Architecture
Provides definePlayer() API for binding XState state machines to the actor base with signal lifecycle and DevTools integration.
Per the Play RFC, this package implements the Logic Layer adapter that transforms declarative machine definitions into live actors with signal-driven reactivity.