Skip to content

@xmachines/play-tanstack-react-router

API / @xmachines/play-tanstack-react-router

TanStack Router (React) adapter for XMachines Play. It keeps the browser URL and the actor state in step through passive infrastructure.

License: MIT Version

Installation

Terminal window
pnpm add @xmachines/play-tanstack-react-router @xmachines/play-router @xmachines/core @xmachines/play @xmachines/play-atom

@xmachines/play-router names @xmachines/core, @xmachines/play and @xmachines/play-atom as peers, so the command installs them with this package.

Peer dependencies. Install them separately:

Terminal window
pnpm add @tanstack/react-router react react-dom

The adapter requires:

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

Usage

PlayRouterProvider is the primary integration point. It creates a TanStackReactRouterBridge on mount. It keeps the bridge connected for the life of the component. It disconnects the bridge on unmount.

import { useMemo, useEffect, useState } from "react";
import { createMachine } from "xstate";
import { createRouter, createRootRoute } from "@tanstack/react-router";
import { PlayRouterProvider } from "@xmachines/play-tanstack-react-router";
import { createRouteMap } from "@xmachines/play-router/xstate";
import { definePlayer, compose, PlayerActor } from "@xmachines/play-xstate";
import { formatPlayRouteTransitions } from "@xmachines/play-xstate/routing";
import { withRouting } from "@xmachines/play-xstate/routing";
import { useAtomEffect } from "@xmachines/play-react";
// Any machine whose states declare `meta.route` (plus an explicit `id`) is routable.
// formatPlayRouteTransitions() generates the root-level `play.route` handlers that
// let the bridge drive the machine from URL changes.
const machine = createMachine(
formatPlayRouteTransitions({
id: "app",
initial: "home",
states: {
home: { id: "home", meta: { route: "/" } },
about: { id: "about", meta: { route: "/about" } },
},
}),
);
const createPlayer = definePlayer({ machine, actor: compose(PlayerActor, withRouting) });
type AppActor = ReturnType<typeof createPlayer>;
// Minimal shell: mirrors the actor's route atom into React state. A full app
// renders <PlayUIProvider> + <PlayRenderer> from @xmachines/play-react here
// instead — see examples/demo for the complete Shell.
function Shell({ actor }: { actor: AppActor }) {
const [route, setRoute] = useState(actor.currentRoute.get());
useAtomEffect(() => setRoute(actor.currentRoute.get()), [actor]);
return <p>Current route: {route}</p>;
}
function createAppRuntime() {
const actor = createPlayer();
actor.start();
const routeMap = createRouteMap(machine);
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) => <Shell actor={currentActor} />}
/>
);
}

Stable references: actor, router, and routeMap must stay stable across the renders. If one prop gets a new identity, the bridge disconnects, then it connects again. Use useMemo to create each prop one time.

TanStackReactRouterBridge — the bridge class

Use the bridge directly when you do not need the React wrapper, or when you integrate it with a custom lifecycle:

import { createRouter, createRootRoute } from "@tanstack/react-router";
import { definePlayer, compose, PlayerActor } from "@xmachines/play-xstate";
import { withRouting } from "@xmachines/play-xstate/routing";
import { TanStackReactRouterBridge } from "@xmachines/play-tanstack-react-router";
import { createRouteMap } from "@xmachines/play-router/xstate";
import { machine } from "./machine.js"; // the routable machine from the example above
const router = createRouter({ routeTree: createRootRoute() });
// The bridge needs the routing capability
const actor = definePlayer({ machine, actor: compose(PlayerActor, withRouting) })();
actor.start();
const routeMap = createRouteMap(machine);
const bridge = new TanStackReactRouterBridge(router, actor, routeMap);
bridge.connect();
// Cleanup when done
bridge.disconnect();

API Summary

TanStackReactRouterBridge

This class extends RouterBridgeBase from @xmachines/play-router. It keeps the actor state atoms and the TanStack Router history in step, in both directions.

class TanStackReactRouterBridge extends RouterBridgeBase {
constructor(router: TanStackRouterLike, actor: RoutableActor, routeMap: RouteMap);
connect(): void; // Start sync; subscribe to router.history and actor atoms
disconnect(): void; // Stop sync; unsubscribe all listeners
}

The bridge subscribes to router.history, not to router.subscribe("onBeforeLoad"). Therefore the bridge also receives a browser BACK or FORWARD navigation (a popstate event) when no <RouterProvider> is mounted.

PlayRouterProvider

This React component 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

The structural type of the router instance. It accepts every object that has the necessary navigate and history shape. A test can therefore use a stub in place of a complete 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;
};
};

Re-exported from @xmachines/play-router

// Route map construction
RouteMap
createRouteMapFromTree(routeTree): RouteMap
// `createRouteMap` and `extractMachineRoutes` read a state machine. This barrel names
// none, so import them from `@xmachines/play-router/xstate`.
// 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 the actor.currentRoute atom changes, the bridge calls router.navigate({ to: path }). The URL then shows the new actor state.
  2. Router → Actor: when router.history.subscribe fires, the bridge sends a play.route event to the actor. A link click, a BACK or FORWARD button, and a call to history.pushState each cause this. The guards of the actor decide if the navigation is valid. The router never enforces the business logic.
  3. Circular update prevention: the lastSyncedPath guard stops a return update. An actor-to-router navigation therefore does not cause an unnecessary router-to-actor send.
  4. Deep-link and restore: on connect(), the bridge reads router.history.location.pathname. That value shows window.location at once, before router.load() runs. The bridge then makes a decision: it sets the actor state from the URL (a deep link), or it writes the restored route of the actor to the URL (a snapshot restore).
  5. Corrective navigation under a mounted <RouterProvider>: for every history event, the bridge calls router.load() and waits for the promise before it synchronizes the actor. A mounted <RouterProvider> defers the refresh of router.latestLocation, and without the wait TanStack drops the corrective navigation for a refused URL. See the @xmachines/play-tanstack-router README for the details.

Demo

examples/demo/ holds a runnable demo of the React and TanStack Router integration. Run it from the repository root:

Terminal window
pnpm install
pnpm --filter @xmachines/play-tanstack-react-router-demo run dev

Then open http://localhost:3011.

The demo shows actor-authoritative routing with a shared auth machine. TanStack Router updates the URL. PlayRouterProvider converts the update into a play.route event. The guards of the actor then permit the access, or they refuse it.

License

MIT — see LICENSE.

@xmachines/play-tanstack-react-router

TanStack Router adapter for the XMachines Play architecture. It keeps the browser URL and the actor state in step through passive infrastructure.

Classes

Interfaces

Type Aliases

Variables

Functions