Skip to content

@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.

License: MIT Version

Part of the XMachines Play monorepo.


Installation

Terminal window
npm install @xmachines/play-xstate xstate

xstate ^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 machine
const machine = setup({}).createMachine({
initial: "idle",
states: {
idle: { meta: { route: "/" } },
active: { meta: { route: "/active" } },
},
});
// 2. Create a player factory
const createPlayer = definePlayer({ machine });
// 3. Instantiate and start an actor
const actor = createPlayer();
actor.start();
// 4. Observe TC39 Signal-based reactive state
console.log(actor.currentRoute.get()); // "/"
console.log(actor.state.get().value); // "idle"
// 5. Send events — machine guards decide transitions
actor.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 instance
const 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 state
const restored = createPlayer({ userId: "alice" }, { snapshot });
restored.start();
console.log(restored.currentRoute.get()); // same route as when saved

PlayerActor<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

SignalTypeDescription
stateSignal.State<SnapshotFrom<TMachine>>Current XState snapshot; updated on every active transition
currentRouteSignal.Computed<string | null>Derived URL from active state’s meta.route template and context
currentViewSignal.State<PlaySpec | null>View spec from active state’s meta.view metadata; enriched with context params
initialRoutereadonly string | nullMachine’s initial-state route (fixed at construction; used by router bridges for deep-link vs restore detection)

Methods

MethodDescription
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.route must also have an explicit id field; omitting it throws MissingStateIdError at machine-definition time.

Other routing exports

ExportDescription
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

Terminal window
# Run tests for this package in isolation
npm test -w packages/play-xstate
# Watch mode
npm run test:watch -w packages/play-xstate

Tests 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.

Classes

Interfaces

Type Aliases

Functions