Skip to content

@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 createBrowserRouter data API.

License: MIT Version


Installation

Terminal window
pnpm add @xmachines/play-react-router

Peer dependencies. Install them if they are not present:

Terminal window
pnpm add react@"^18 || ^19" react-router@"^7.0.0" xstate@"^5.31.0"

Usage

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 with useMemo. 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 no subscribe or navigate API.

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 subscriptions

API

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.

MethodDescription
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

ExportDescription
PlayRouterProviderPropsProps interface for PlayRouterProvider
PlayActorThe constraint type for an actor that PlayRouterProvider and the bridge accept

Route map utilities (re-exported from @xmachines/play-router)

ExportDescription
RouteMapBidirectional 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
RouteMapOptionsOptions type for createRouteMap
RouteMappingType for a single { stateId, path } entry
RouterBridgeInterface that ReactRouterBridge satisfies
PlayRouteEventThe play.route event type that a router sends to an actor on navigation

Testing

Run tests for this package in isolation:

Terminal window
pnpm --filter @xmachines/play-react-router test

Or from inside the package directory:

Terminal window
pnpm test

Tests 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().

Terminal window
# Run browser tests only
pnpm exec vitest --config vitest.browser.config.ts --project play-react-router-browser

Coverage thresholds (v8 provider):

TypeThreshold
Lines80%
Functions80%
Branches80%
Statements80%

License

MIT

@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

Type Aliases

Variables

Functions