@xmachines/play-react-router
API / @xmachines/play-react-router
React Router v7 adapter for the XMachines Play Universal Player Architecture. It keeps the actor state and the browser URL in step, in both directions, through the
createBrowserRouterdata API.
Installation
pnpm add @xmachines/play-react-routerPeer dependencies. Install them if they are not present:
pnpm add react@"^18 || ^19" react-router@"^7.0.0" xstate@"^5.31.0"Usage
PlayRouterProvider — Recommended (React component)
PlayRouterProvider connects a PlayerActor to React Router inside a React component tree. It creates a ReactRouterBridge on mount. It keeps the actor state and the browser URL in step, in both directions. It disconnects the bridge on unmount.
All three props (
actor,router,routeMap) must be stable references. Create them outside the JSX, or hold them withuseMemo. An inline value makes the bridge disconnect and connect again on every render.
import { useMemo, useEffect } from "react";import { createBrowserRouter, RouterProvider } from "react-router";import { PlayRouterProvider, createRouteMap } from "@xmachines/play-react-router";import { definePlayer } from "@xmachines/play-xstate";import { myMachine } from "./machine.js";
const createPlayer = definePlayer({ machine: myMachine });const routeMap = createRouteMap(myMachine);
// Minimal app shell stub — a real app renders PlayUIProvider + PlayRenderer from// @xmachines/play-react here (see the workspace-only @xmachines/play-react-demo Shell)function App({ actor }: { actor: ReturnType<typeof createPlayer> }) { return <main />; // render your UI from the actor here}
function createAppRuntime() { const actor = createPlayer(); actor.start(); const router = createBrowserRouter([{ path: "*", element: <App actor={actor} /> }]); return { actor, router };}
export default function Root() { const { actor, router } = useMemo(createAppRuntime, []);
useEffect(() => () => actor.stop(), [actor]);
return ( <PlayRouterProvider actor={actor} router={router} routeMap={routeMap} renderer={(_, currentRouter) => <RouterProvider router={currentRouter} />} /> );}ReactRouterBridge — Manual (imperative API)
Use ReactRouterBridge directly when you need imperative lifecycle control outside React.
The bridge requires
createBrowserRouter(the data router API). It does not support the old<BrowserRouter>component, because that component has nosubscribeornavigateAPI.
import { createBrowserRouter } from "react-router";import { ReactRouterBridge, createRouteMap } from "@xmachines/play-react-router";import { definePlayer } from "@xmachines/play-xstate";import { myMachine } from "./machine.js";
const actor = definePlayer({ machine: myMachine })();actor.start();
const router = createBrowserRouter([/* routes */]);const routeMap = createRouteMap(myMachine);
const bridge = new ReactRouterBridge(router, actor, routeMap);bridge.connect(); // starts bidirectional sync// ... later:bridge.disconnect(); // stops sync and cleans up subscriptionsAPI
PlayRouterProvider
A React component that manages a ReactRouterBridge lifecycle via useEffect.
interface PlayRouterProviderProps<TActor> { /** The actor to sync with React Router. Must be a stable reference. */ actor: TActor; /** The React Router instance returned by `createBrowserRouter`. */ router: BrowserRouterInstance; /** * Bidirectional route map for state ID ↔ URL path lookups. * Must be a stable reference — memoize with useMemo if constructed inline. */ routeMap: RouteMap; /** Render callback — receives the actor and router. */ renderer: (actor: TActor, router: BrowserRouterInstance) => ReactNode;}ReactRouterBridge
Extends RouterBridgeBase from @xmachines/play-router. Implements the RouterBridge protocol.
| Method | Description |
|---|---|
connect() | Subscribes to the router changes and sets the actor state from the current URL |
disconnect() | Cancels the subscription and stops all synchronization |
Types exported from this package
| Export | Description |
|---|---|
PlayRouterProviderProps | Props interface for PlayRouterProvider |
PlayActor | The constraint type for an actor that PlayRouterProvider and the bridge accept |
Route map utilities (re-exported from @xmachines/play-router)
| Export | Description |
|---|---|
RouteMap | Bidirectional state ID ↔ URL path map |
createRouteMap(machine) | Build a RouteMap directly from an XState machine definition |
createRouteMapFromTree(tree) | Build a RouteMap from a RouteTree object |
RouteMapOptions | Options type for createRouteMap |
RouteMapping | Type for a single { stateId, path } entry |
RouterBridge | Interface that ReactRouterBridge satisfies |
PlayRouteEvent | The play.route event type that a router sends to an actor on navigation |
Testing
Run tests for this package in isolation:
pnpm --filter @xmachines/play-react-router testOr from inside the package directory:
pnpm testTests use Vitest. Component tests (*.test.tsx) run in a jsdom environment via @testing-library/react. Unit tests (*.test.ts) run in Node.
Browser tests (test/browser/**/*.browser.test.ts) run in real Chromium through Playwright. They cover the asynchronous sequences that jsdom cannot reproduce: BACK and FORWARD navigation, the callback order of router.subscribe, echo suppression under real microtask timing, and subscriber teardown on disconnect().
# Run browser tests onlypnpm exec vitest --config vitest.browser.config.ts --project play-react-router-browserCoverage thresholds (v8 provider):
| Type | Threshold |
|---|---|
| Lines | 80% |
| Functions | 80% |
| Branches | 80% |
| Statements | 80% |
License
@xmachines/play-react-router
React Router v7 adapter for the XMachines Play architecture. It keeps the browser URL and the actor state in step through the createBrowserRouter data API.
Classes
Interfaces
- PlayActor
- PlayRouteEvent
- PlayRouterProviderBaseProps
- PlayRouterProviderProps
- RouteMapOptions
- RouteMapping
- RouterBridge