@xmachines/play-react-router
API / @xmachines/play-react-router
React Router v7 adapter for the XMachines Play Universal Player Architecture — synchronizes actor state with the browser URL bidirectionally using the
createBrowserRouterdata API.
Part of the XMachines Play monorepo.
Installation
npm install @xmachines/play-react-routerPeer dependencies — install if not already present:
npm install 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, keeps actor state and the browser URL in sync bidirectionally, and tears the bridge down cleanly on unmount.
All three props (
actor,router,routeMap) must be stable references. Create them outside of JSX or memoize withuseMemo. Recreating them inline triggers a bridge disconnect/reconnect 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);
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.
Requires
createBrowserRouter(data router API). The legacy<BrowserRouter>component is not supported — it does not expose thesubscribe/navigateAPI.
import { createBrowserRouter } from "react-router";import { ReactRouterBridge, createRouteMap } from "@xmachines/play-react-router";import { myMachine } from "./machine.js";
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 router changes and syncs actor state from the current URL |
disconnect() | Unsubscribes and stops all synchronization |
Types exported from this package
| Export | Description |
|---|---|
PlayRouterProviderProps | Props interface for PlayRouterProvider |
PlayActor | Constraint type for actors accepted by PlayRouterProvider and the bridge |
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 sent to actors on navigation |
Testing
Run tests for this package in isolation:
npm test -w packages/play-react-routerOr from inside the package directory:
npm 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 against real Chromium via Playwright, covering async sequencing that jsdom cannot faithfully reproduce: BACK/FORWARD navigation, router.subscribe callback ordering, echo suppression under real microtask timing, and subscriber teardown on disconnect().
# Run browser tests onlynpx 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 XMachines Play architecture. Synchronizes browser URL with actor state using createBrowserRouter data API.