@xmachines/play-tanstack-react-router
API / @xmachines/play-tanstack-react-router
TanStack Router (React) adapter for XMachines Play — synchronizes browser URL with actor state through passive infrastructure.
Part of the xmachines-js monorepo.
Installation
npm install @xmachines/play-tanstack-react-routerPeer dependencies (install separately):
npm install @tanstack/react-router react react-dom xstateRequires:
@tanstack/react-router^1.168.8react^18.0.0or^19.0.0react-dom^18.0.0or^19.0.0xstate^5.31.0
Usage
PlayRouterProvider — React component (recommended)
PlayRouterProvider is the primary integration point. It creates a TanStackReactRouterBridge on mount, keeps it connected for the component lifetime, and tears it down on unmount.
import { useMemo, useEffect } from "react";import { createRouter, createRootRoute } from "@tanstack/react-router";import { PlayRouterProvider, createRouteMapFromTree, extractMachineRoutes,} from "@xmachines/play-tanstack-react-router";import { definePlayer } from "@xmachines/play-xstate";
function createAppRuntime() { const actor = definePlayer({ machine, catalog })(); actor.start();
const routeTree = extractMachineRoutes(machine); const routeMap = createRouteMapFromTree(routeTree); const rootRoute = createRootRoute(); const router = createRouter({ routeTree: rootRoute });
return { actor, routeMap, router };}
export function App() { // All three props must be stable references — memoize to avoid reconnecting on every render const { actor, routeMap, router } = useMemo(createAppRuntime, []);
useEffect(() => () => actor.stop(), [actor]);
return ( <PlayRouterProvider actor={actor} router={router} routeMap={routeMap} renderer={(currentActor, currentRouter) => ( <Shell actor={currentActor} router={currentRouter} /> )} /> );}Stable references:
actor,router, androuteMapmust be stable across renders. If any prop changes identity, the bridge disconnects and reconnects. UseuseMemoto create them once.
TanStackReactRouterBridge — headless class
Use the bridge directly when you don’t need the React wrapper, or when integrating with a custom lifecycle:
import { createRouter } from "@tanstack/react-router";import { definePlayer } from "@xmachines/play-xstate";import { TanStackReactRouterBridge, createRouteMapFromTree, extractMachineRoutes,} from "@xmachines/play-tanstack-react-router";
const router = createRouter({ routeTree });const actor = definePlayer({ machine, catalog })();const routeMap = createRouteMapFromTree(extractMachineRoutes(machine));
const bridge = new TanStackReactRouterBridge(router, actor, routeMap);bridge.connect();
// Cleanup when donebridge.disconnect();API Summary
TanStackReactRouterBridge
Extends RouterBridgeBase from @xmachines/play-router. Implements bidirectional sync between actor state signals and TanStack Router history.
class TanStackReactRouterBridge extends RouterBridgeBase { constructor(router: TanStackRouterLike, actor: RoutableActor, routeMap: RouteMap);
connect(): void; // Start sync; subscribe to router.history and actor signals disconnect(): void; // Stop sync; unsubscribe all listeners}Subscribes to router.history (not router.subscribe("onBeforeLoad")) so that back/forward browser navigation (popstate events) are captured even without a <RouterProvider> mounted.
PlayRouterProvider
React component that wraps TanStackReactRouterBridge in a useEffect lifecycle.
interface PlayRouterProviderProps<TActor> { actor: TActor; // Must be stable router: TanStackRouterInstance; // Must be stable routeMap: RouteMap; // Must be stable renderer: (actor: TActor, router: TanStackRouterInstance) => ReactNode;}TanStackRouterLike
Structural type for the router instance — accepts any object with the required navigate and history shape, making it testable without creating a full TanStack Router:
type TanStackRouterLike = { navigate(args: { to: string }): void; load?(): void | Promise<void>; history: { location: { pathname: string; search?: string }; subscribe( handler: (event: { location: { pathname: string; search?: string } }) => void, ): () => void; };};RouteNavigateEvent
Event sent to the actor when the browser navigates:
interface RouteNavigateEvent { readonly type: "route.navigate"; readonly path: string; // e.g. "/dashboard" or "/posts/123"}Re-exported from @xmachines/play-router
// Route map constructionRouteMapcreateRouteMap(mappings: RouteMapping[]): RouteMapcreateRouteMapFromTree(routeTree): RouteMapextractMachineRoutes(machine): RouteTree
// Typestype RouteMapOptionstype RouteMappingtype RouterBridgetype PlayRouteEventHow It Works
The bridge implements the Passive Infrastructure invariant from the XMachines RFC:
- Actor → Router: When
actor.currentRoutesignal changes, the bridge callsrouter.navigate({ to: path }). The URL updates to reflect the new actor state. - Router → Actor: When
router.history.subscribefires (link clicks,back/forwardbuttons,history.pushState), the bridge sends aplay.routeevent to the actor. The actor’s guards decide whether navigation is valid — the router never enforces business logic. - Circular update prevention: A
lastSyncedPathguard suppresses round-trip updates so an actor → router navigation does not trigger a redundant router → actor send. - Deep-link and restore: On
connect(), the bridge readsrouter.history.location.pathname(which reflectswindow.locationimmediately, beforerouter.load()runs) and determines whether to sync the actor from the URL (deep-link) or push the actor’s restored route to the URL (snapshot restore).
Testing
Run tests for this package in isolation:
npm test -w @xmachines/play-tanstack-react-routerFrom the monorepo root:
npm testTests cover RouterBridge protocol compliance, actor ↔ router bidirectional sync, circular update prevention, deep-link and snapshot-restore scenarios, and PlayRouterProvider lifecycle (mount/unmount/reconnect).
Demo
A runnable React + TanStack Router integration demo is available under examples/demo/. To run it from the repository root:
npm installnpm run dev -w @xmachines/play-tanstack-react-router-demoThen open http://localhost:3000.
The demo shows actor-authoritative routing with a shared auth machine: TanStack Router updates the URL, PlayRouterProvider translates it to a play.route event, and the actor’s guards decide whether access is permitted.
License
MIT — see LICENSE.
@xmachines/play-tanstack-react-router
TanStack Router adapter for XMachines Play architecture. Synchronizes browser URL with actor state through passive infrastructure.
Classes
Interfaces
- PlayActor
- PlayRouteEvent
- PlayRouterProviderProps
- RouteMapOptions
- RouteMapping
- RouteNavigateEvent
- RouterBridge