Play - Universal Frontend
Play is XMachines’ approach to building frontend applications that work identically across any JavaScript runtime—browser, Node, Deno, edge functions—with zero code changes.
The Problem
Section titled “The Problem”Most frontend frameworks tightly couple business logic to:
- Specific routing libraries
- Browser APIs
- Framework-specific state management
- View layer implementations
This makes it difficult to:
- Test business logic in isolation
- Reuse logic across platforms (web, mobile, desktop)
- Build truly universal apps (SSR + CSR + SSG with same code)
- Let AI agents control navigation based on business rules
Play’s Solution: Strict Separation
Section titled “Play’s Solution: Strict Separation”Play enforces a strict separation between three concerns:
graph LR A[Actor<br/>Business Logic] -->|Signals| B[Runtime Adapter<br/>Infrastructure] B -->|Events| A A -->|Signals| C[View<br/>Presentation]The Three Roles
Section titled “The Three Roles”1. The Actor (Business Logic Engine)
- Pure, environment-agnostic logic runtime
- Owns state, guards, errors, and route validity
- Emits Virtual Routes as derived intent
- Has zero knowledge of: Browser APIs, routing libraries, view frameworks
2. The Runtime Adapter (Infrastructure Layer)
- Environment-specific adapter (Browser, Native, Server, Test Runner)
- Reflects Actor output into the environment
- Forwards environment events to the Actor without interpretation
- Never enforces guards, validation, or business rules
3. The View (Passive Presentation)
- Passive consumer of Actor state
- No business rules or routing authority
- Renders based solely on Actor signals
Communication Medium: TC39 Signals
Section titled “Communication Medium: TC39 Signals”Play uses TC39 Signals exclusively for Actor ↔ Adapter ↔ View communication:
- Push-pull reactive model
- Synchronous, glitch-free propagation
- Standard, framework-agnostic API
Core Principles
Section titled “Core Principles”1. Actor Authority
Section titled “1. Actor Authority”The Actor is the final authority on state and route validity.
If the browser says “navigate to /admin” but the Actor rejects it (user not authenticated), the Actor:
- Transitions to an error or fallback state (e.g.,
/login) - Emits the corrected Virtual Route
- Forces the browser to realign with Actor reality
2. Logic-Driven Navigation
Section titled “2. Logic-Driven Navigation”Routes are derived from state, not configured separately:
states: { authenticated: { meta: { route: '/dashboard', // Virtual Route view: { type: 'Dashboard', props: { ... } } } }, unauthenticated: { meta: { route: '/login', view: { type: 'LoginForm', props: { ... } } } }}3. State-Driven Reset
Section titled “3. State-Driven Reset”Invalid external navigation is always overwritten by Actor-derived state.
Package Model
Section titled “Package Model”Play is distributed as multiple focused packages:
| Package | Purpose |
|---|---|
@xmachines/play-signals | TC39 Signals implementation |
@xmachines/play-ui | JSON schema + validation (Zod) |
@xmachines/play-actor | Abstract Actor interface |
@xmachines/play-xstate | XState v5 adapter + definePlayer |
@xmachines/play-router | Route tree protocol |
@xmachines/play-tanstack-router | TanStack Router adapter |
@xmachines/play-react | PlayRenderer component |
This allows you to:
- Swap state engines (XState, custom, etc.)
- Swap routers (TanStack, React Router, custom)
- Swap view layers (React, Vue, custom)
- Use only the pieces you need
Quick Start
Section titled “Quick Start”Installation
Section titled “Installation”npm install @xmachines/play-xstate @xmachines/play-react @xmachines/play-tanstack-router1. Define Your Player (Logic + Schema)
Section titled “1. Define Your Player (Logic + Schema)”features/dashboard/player.ts - Pure logic, no React, no routing:
import { definePlayer } from '@xmachines/play-xstate';import { defineCatalog, z } from '@xmachines/play-ui';
// 1. Define the UI vocabulary (schema)export const catalog = defineCatalog({ components: { Dashboard: { props: z.object({ title: z.string() }) }, Metric: { props: z.object({ value: z.number(), label: z.string() }) } }});
// 2. Define the business logic (state machine)export const player = definePlayer({ catalog, machine: { initial: 'overview', states: { overview: { meta: { route: '/dashboard', view: { type: 'Dashboard', props: { title: 'Q4 Performance' }, children: [ { type: 'Metric', props: { value: 100, label: 'Sales' } } ] } } } } }});2. Create React Implementation
Section titled “2. Create React Implementation”App.tsx - Bind logic to React and Router:
import { player, catalog } from './features/dashboard/player';import { PlayRenderer } from '@xmachines/play-react';import { createPlayRouter } from '@xmachines/play-tanstack-router';import { defineComponents } from '@json-render/react';import { RouterProvider } from '@tanstack/react-router';
// 1. Define React component implementationsconst components = defineComponents(catalog, { Dashboard: ({ props, children }) => ( <div className="dashboard"> <h1>{props.title}</h1> {children} </div> ), Metric: ({ props }) => ( <span className="metric"> {props.label}: {props.value} </span> )});
// 2. Create router with type-safe bindingsconst router = createPlayRouter({ player, components, // Type-checked against catalog component: PlayRenderer});
// 3. Renderexport function App() { return <RouterProvider router={router} />;}That’s it! Your business logic is completely decoupled from React and routing.
Use Cases
Section titled “Use Cases”Universal Rendering
Section titled “Universal Rendering”Same business logic works for:
- SSR (server-side rendering)
- CSR (client-side rendering)
- SSG (static site generation)
- Hybrid (islands architecture)
Multi-Platform
Section titled “Multi-Platform”- Web (browser)
- Mobile (React Native)
- Desktop (Electron)
- Edge functions (Cloudflare Workers, Vercel Edge)
AI-Driven Navigation
Section titled “AI-Driven Navigation”Let AI agents control navigation by sending events to the Actor. The Actor’s business rules determine valid transitions.
Testing
Section titled “Testing”Test business logic in isolation—no DOM, no routing library, just pure state machine logic:
import { createActor } from 'xstate';import { player } from './player';
test('navigation requires auth', () => { const actor = player.create({ /* ... */ }); actor.start();
// Attempt to navigate without auth actor.send({ type: 'NAVIGATE', path: '/dashboard' });
// Actor rejects and redirects to login expect(actor.currentRoute.value).toBe('/login');});Architecture Deep Dive
Section titled “Architecture Deep Dive”For complete technical specifications, see the Play v1 RFC.
Key Topics Covered in RFC:
Section titled “Key Topics Covered in RFC:”- Detailed architecture roles and responsibilities
- Signal-based reactive substrate
- Virtual Route protocol
- Package breakdown and interfaces
- Type safety guarantees
- Lock statements (invariants)
Learn More
Section titled “Learn More”- RFC: Play v1 - Complete technical specification
- Core Concepts - Understand XMachines fundamentals
- Platform Overview - Other platform options