@xmachines/play-react-router
v2.0.0
Published
React Router v7 adapter for @xmachines/play Universal Player Architecture.
Maintainers
Readme
@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
pnpm add @xmachines/play-react-routerPeer dependencies — install if not already 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, 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:
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 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 only
pnpm 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% |
