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.
- Actor authority — the Actor is the final authority on the state and on the validity of a route.
- Strict separation — the business logic does not use a runtime API or a router library.
- One reactive medium — the roles communicate through one reactive primitive only.
- Passive infrastructure — a Runtime Adapter does not apply guards, validation, or business rules.
- 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 firedactor.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
| Package | Role |
|---|---|
@xmachines/play-xstate | Runs an XState v5 machine as a Play Actor, through definePlayer |
@xmachines/play-actor | The Actor contract. It names no state machine library |
@xmachines/play-router | Reads the routes from the machine, and holds the shared base of the router adapters |
@xmachines/play | The shared protocol types, such as PlayEvent and PlayError |
Router adapters
| Router | Package |
|---|---|
| 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 framework | Package |
|---|---|
| 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
- Getting Started — install the packages and build a first application
- The Actor Model — the Actor and the split between logic and infrastructure
- State Machines — how XMachines uses XState v5
- Examples — a demo application for each router and each renderer
- RFC: Play — the complete specification
- Platform Overview — the other platforms