Skip to content

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

Examples / @xmachines/play-tanstack-react-router-demo

React + TanStack Router integration demo for the XMachines Play architecture with actor-authoritative routing.

License: MIT

What This Demonstrates

  • Shared auth machine reused without framework-specific business logic
  • PlayRouterProvider renderer-based integration with TanStack Router
  • Shell-driven rendering via PlayRenderer with actor-authoritative navigation
  • Non-browser invariant tests plus browser E2E coverage
  • Stately inspector attached at creation via options.inspect — the debug panel’s Show Inspector button opens the live machine in its own window

Running the Demo

From the repository root:

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

Then open http://localhost:3011.

Step-by-Step Code Flow

Use this order to understand the implementation:

  1. src/main.tsx mounts <App />.
  2. src/App.tsx calls useMemo(createAppRuntime, []) to create the actor from the shared machine and start it once per mounted app.
  3. src/runtime.ts exports inspector (via createDemoInspector()), createPlayer (via definePlayer, with options.inspect wired to the inspector), routeMap (via createRouteMap(authMachine)), and createRegistryResult which binds UI components and actor event actions.
  4. createAppRuntime() constructs a TanStack createRootRoute/createRouter instance, then PlayRouterProvider bridges it to the actor and renders Shell via renderer(actor, router).
  5. Shell is the shared demo layout component imported from @xmachines/play-react-demo (defined in packages/play-react/examples/demo/src/components/Shell.tsx, a workspace-only package). It renders PlayUIProvider + PlayRenderer, header/nav, and the debug panel — including the Show Inspector button — from actor state.
  6. Tests in test/ and test/browser/ validate invariant and runtime behavior.
// src/App.tsx (shape)
import { Shell } from "@xmachines/play-react-demo"; // shared demo shell (workspace-only package)
import { createPlayer, inspector, routeMap, createRegistryResult } from "./runtime.js";
function createAppRuntime() {
const actor = createPlayer();
actor.start();
const registryResult = createRegistryResult(actor);
const rootRoute = createRootRoute();
const router = createRouter({ routeTree: rootRoute, defaultPreload: "intent" });
return { actor, router, registryResult };
}
const { actor, router, registryResult } = useMemo(createAppRuntime, []);
return (
<PlayRouterProvider
actor={actor}
router={router}
routeMap={routeMap}
renderer={(currentActor, currentRouter) => (
<Shell
actor={currentActor}
router={currentRouter}
registryResult={registryResult}
inspector={inspector}
/>
)}
/>
);
// src/runtime.ts (shape)
export const inspector = createDemoInspector();
export const createPlayer = definePlayer({
machine: authMachine,
options: { inspect: inspector.inspect },
actor: compose(PlayerActor, withRouting, withView),
});
export const routeMap = createRouteMap(authMachine);
export function createRegistryResult(actor: ReturnType<typeof createPlayer>) {
return defineRegistry(authCatalog, {
components: demoComponents,
actions: createAuthActionHandlers(actor),
});
}

Key Files

  • src/main.tsx - React entry point that mounts <App />
  • src/App.tsx - actor lifecycle, provider wiring, and Shell rendering
  • src/runtime.ts - actor factory, route map creation, and registry/action binding
  • test/actor-authority.test.ts - actor authority and guarded navigation behavior
  • test/passive-infra.test.ts - passive infrastructure separation contracts
  • test/strict-separation.test.ts - machine/view infrastructure separation contracts
  • test/atom-only.test.ts - atom-only reactivity invariant
  • test/route-parameters.test.ts - route parameter handling
  • test/invalid-route-redirect.test.ts - invalid route redirect behavior
  • test/xstate-route-events.test.ts - XState route event contracts
  • test/browser/shared-demo.browser.test.tsx - canonical browser auth flow
  • test/browser/auth-flow.browser.test.tsx - extended browser auth flow checks
  • test/browser/back-forward-sync.browser.test.tsx - URL sync and history behavior
  • test/browser/dashboard-logout.browser.test.tsx - logout and redirection flow
  • test/browser/guard-rejection.browser.test.tsx - guard-driven rejection in browser
  • test/browser/login-flow.browser.test.tsx - login flow browser coverage
  • test/browser/navigation.browser.test.tsx - navigation behavior in browser
  • test/browser/settings-parameter.browser.test.tsx - parameterized route in browser
  • test/browser/invalid-route-redirect.browser.test.tsx - invalid route redirect in browser
  • test/browser/tanstack-integration.browser.test.tsx - TanStack Router integration checks

State Machine & Architecture Details

The demo utilizes XMachines architectural invariants:

  1. Actor Authority: When a user navigates to a protected route, TanStack Router updates the URL. The PlayRouterProvider intercepts this, translates it to a play.route event, and sends it to the actor. The actor evaluates guards (e.g. isAuthenticated) and executes state transitions.
  2. Passive Infrastructure: The router does not execute route loaders or guards for business logic. The actor dictates whether navigation is permitted.
  3. Atom-Only Reactivity: The bridge leverages React’s internal mechanisms combined with TanStack’s router.subscribe to react precisely when atoms update, without relying on useEffect synchronization inside the application codebase.

Watcher Lifecycle and Cleanup Contract

This demo follows the canonical watcher lifecycle:

  1. The atom notifies the subscription, synchronously, from the set that changed it
  2. watchAtom queues one microtask, and it drops each duplicate notification of that batch
  3. The microtask reads the actor atoms and triggers React state updates

One subscription lives for the whole watch, and no caller arms a watcher again. Cleanup is explicit and lifecycle-bound: the provider automatically triggers bridge.disconnect() when the component tree unmounts to prevent memory leaks and zombie subscriptions.

Adapter Boundaries

The TanStack React adapter wraps TanStack’s router.navigate({ to }) and router.subscribe methods while delegating core synchronization policy to RouterBridgeBase.

Available Scripts

These commands are defined in package.json:

CommandDescription
pnpm --filter @xmachines/play-tanstack-react-router-demo run devStart Vite dev server (http://localhost:3011/)
pnpm --filter @xmachines/play-tanstack-react-router-demo run buildBuild production bundle
pnpm --filter @xmachines/play-tanstack-react-router-demo run previewPreview built bundle
pnpm --filter @xmachines/play-tanstack-react-router-demo run testRun Vitest test suite
pnpm --filter @xmachines/play-tanstack-react-router-demo run test:browserRun browser-focused Vitest suite

Learn More