Skip to content

@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

Terminal window
npm install @xmachines/play-tanstack-react-router

Peer dependencies (install separately):

Terminal window
npm install @tanstack/react-router react react-dom xstate

Requires:

  • @tanstack/react-router ^1.168.8
  • react ^18.0.0 or ^19.0.0
  • react-dom ^18.0.0 or ^19.0.0
  • xstate ^5.31.0

Usage

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, and routeMap must be stable across renders. If any prop changes identity, the bridge disconnects and reconnects. Use useMemo to 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 done
bridge.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 construction
RouteMap
createRouteMap(mappings: RouteMapping[]): RouteMap
createRouteMapFromTree(routeTree): RouteMap
extractMachineRoutes(machine): RouteTree
// Types
type RouteMapOptions
type RouteMapping
type RouterBridge
type PlayRouteEvent

How It Works

The bridge implements the Passive Infrastructure invariant from the XMachines RFC:

  1. Actor → Router: When actor.currentRoute signal changes, the bridge calls router.navigate({ to: path }). The URL updates to reflect the new actor state.
  2. Router → Actor: When router.history.subscribe fires (link clicks, back/forward buttons, history.pushState), the bridge sends a play.route event to the actor. The actor’s guards decide whether navigation is valid — the router never enforces business logic.
  3. Circular update prevention: A lastSyncedPath guard suppresses round-trip updates so an actor → router navigation does not trigger a redundant router → actor send.
  4. Deep-link and restore: On connect(), the bridge reads router.history.location.pathname (which reflects window.location immediately, before router.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:

Terminal window
npm test -w @xmachines/play-tanstack-react-router

From the monorepo root:

Terminal window
npm test

Tests 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:

Terminal window
npm install
npm run dev -w @xmachines/play-tanstack-react-router-demo

Then 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

Type Aliases

Functions