Skip to content

Play

Play is the XMachines platform for frontend applications. It is the first platform with published packages.

In a Play application, a state machine holds all of the business logic. It decides the state, the route, and the view. The router and the renderer only reflect what the machine decides. You can replace either of them, and the machine does not change.

The Problem

In most frontend applications, the business logic depends on the infrastructure around it:

  • The route configuration of a router library
  • The browser APIs
  • The state management of a view framework

Logic of this type is difficult to test without a browser. It is also difficult to move to a different router or view framework, and difficult to let an agent drive, because the rules are in many places.

Three Roles

Play divides an application into three roles.

graph LR
    A[Actor<br/>Business logic] -->|state, route, view| B[Runtime Adapter<br/>Router]
    B -->|navigation events| A
    A -->|state, route, view| C[View<br/>Renderer]

The Actor is the business logic. An XState v5 machine runs inside it. The Actor owns the state, the guards, the errors, and the validity of each route. It does not know the browser, the router, or the view framework.

The Runtime Adapter connects the Actor to a router. It shows the route of the Actor in the URL. It sends each navigation from the browser to the Actor as an event, and it does not interpret the event.

The View renders the view of the Actor. It holds no business rules, and it cannot change the route.

Invariants

The Play RFC defines five invariants. Every Play package obeys them.

  1. Actor authority — the Actor is the final authority on the state and on the validity of a route.
  2. Strict separation — the business logic does not use a runtime API or a router library.
  3. One reactive medium — the roles communicate through one reactive primitive only.
  4. Passive infrastructure — a Runtime Adapter does not apply guards, validation, or business rules.
  5. State-driven reset — when an external navigation is not valid, the Actor replaces it with its own route.

Routes Come From the State

A state node declares its route in meta.route. The router configuration is not a separate file: the Play router packages read the routes from the machine.

states: {
home: { id: 'home', meta: { route: '/' } },
login: { id: 'login', meta: { route: '/login' } },
dashboard: {
id: 'dashboard',
meta: { route: '/dashboard' },
// The guard sends a user who is not signed in to the login page.
always: {
guard: ({ context }) => !context.isAuthenticated,
target: 'login',
},
},
},

A navigation is an event. The Actor applies its guards, and the route follows the resulting state:

actor.send({ type: 'play.route', to: '#dashboard' });
actor.getSnapshot().value; // 'login', because the guard fired
actor.currentRoute.get(); // '/login'

When the browser opens /dashboard directly, the same guard applies. The Actor moves to login, and the Runtime Adapter changes the URL to /login.

The view comes from the state in the same way. A state node can declare a view spec, and a renderer turns the spec into components. The specs use the JSON-Render format.

Packages

Play is a set of small packages. Install the logic packages, one router adapter, and one renderer.

Logic

PackageRole
@xmachines/play-xstateRuns an XState v5 machine as a Play Actor, through definePlayer
@xmachines/play-actorThe Actor contract. It names no state machine library
@xmachines/play-routerReads the routes from the machine, and holds the shared base of the router adapters
@xmachines/playThe shared protocol types, such as PlayEvent and PlayError

Router adapters

RouterPackage
TanStack Router (React)@xmachines/play-tanstack-react-router
TanStack Router (SolidJS)@xmachines/play-tanstack-solid-router
React Router@xmachines/play-react-router
Vue Router@xmachines/play-vue-router
SolidJS Router@xmachines/play-solid-router
SvelteKit@xmachines/play-sveltekit-router
svelte-spa-router@xmachines/play-svelte-spa-router
Browser History API, with no framework@xmachines/play-dom-router

Renderers

View frameworkPackage
React@xmachines/play-react
Vue 3@xmachines/play-vue
SolidJS@xmachines/play-solid
Svelte 5@xmachines/play-svelte
DOM, with no framework@xmachines/play-dom

The reactive primitive changed in version 5.0. Versions before 5.0 use TC39 Signals from @xmachines/play-signals. Version 5.0 and later use atoms from @xmachines/play-atom, over @xstate/store. Each version also adds packages of its own. The API reference lists the packages of the version that you read.

Get Started

The Getting Started guide of each version gives the exact install command. It then builds an Actor, connects a router, and renders a view.

Because the Actor uses no browser API, you can test the business logic in Node with no DOM and no router. Send events to the Actor, and read its state and its route.

Learn More