@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.
What This Demonstrates
- Shared auth machine reused without framework-specific business logic
PlayRouterProviderrenderer-based integration with TanStack Router- Shell-driven rendering via
PlayRendererwith 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:
pnpm installpnpm --filter @xmachines/play-tanstack-react-router-demo run devThen open http://localhost:3011.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tsxmounts<App />.src/App.tsxcallsuseMemo(createAppRuntime, [])to create the actor from the shared machine and start it once per mounted app.src/runtime.tsexportsinspector(viacreateDemoInspector()),createPlayer(viadefinePlayer, withoptions.inspectwired to the inspector),routeMap(viacreateRouteMap(authMachine)), andcreateRegistryResultwhich binds UI components and actor event actions.createAppRuntime()constructs a TanStackcreateRootRoute/createRouterinstance, thenPlayRouterProviderbridges it to the actor and rendersShellviarenderer(actor, router).Shellis the shared demo layout component imported from@xmachines/play-react-demo(defined inpackages/play-react/examples/demo/src/components/Shell.tsx, a workspace-only package). It rendersPlayUIProvider+PlayRenderer, header/nav, and the debug panel — including the Show Inspector button — from actor state.- Tests in
test/andtest/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, andShellrenderingsrc/runtime.ts- actor factory, route map creation, and registry/action bindingtest/actor-authority.test.ts- actor authority and guarded navigation behaviortest/passive-infra.test.ts- passive infrastructure separation contractstest/strict-separation.test.ts- machine/view infrastructure separation contractstest/atom-only.test.ts- atom-only reactivity invarianttest/route-parameters.test.ts- route parameter handlingtest/invalid-route-redirect.test.ts- invalid route redirect behaviortest/xstate-route-events.test.ts- XState route event contractstest/browser/shared-demo.browser.test.tsx- canonical browser auth flowtest/browser/auth-flow.browser.test.tsx- extended browser auth flow checkstest/browser/back-forward-sync.browser.test.tsx- URL sync and history behaviortest/browser/dashboard-logout.browser.test.tsx- logout and redirection flowtest/browser/guard-rejection.browser.test.tsx- guard-driven rejection in browsertest/browser/login-flow.browser.test.tsx- login flow browser coveragetest/browser/navigation.browser.test.tsx- navigation behavior in browsertest/browser/settings-parameter.browser.test.tsx- parameterized route in browsertest/browser/invalid-route-redirect.browser.test.tsx- invalid route redirect in browsertest/browser/tanstack-integration.browser.test.tsx- TanStack Router integration checks
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants:
- Actor Authority: When a user navigates to a protected route, TanStack Router updates the URL. The
PlayRouterProviderintercepts this, translates it to aplay.routeevent, and sends it to the actor. The actor evaluates guards (e.g.isAuthenticated) and executes state transitions. - Passive Infrastructure: The router does not execute route loaders or guards for business logic. The actor dictates whether navigation is permitted.
- Atom-Only Reactivity: The bridge leverages React’s internal mechanisms combined with TanStack’s
router.subscribeto react precisely when atoms update, without relying onuseEffectsynchronization inside the application codebase.
Watcher Lifecycle and Cleanup Contract
This demo follows the canonical watcher lifecycle:
- The atom notifies the subscription, synchronously, from the
setthat changed it watchAtomqueues one microtask, and it drops each duplicate notification of that batch- 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:
| Command | Description |
|---|---|
pnpm --filter @xmachines/play-tanstack-react-router-demo run dev | Start Vite dev server (http://localhost:3011/) |
pnpm --filter @xmachines/play-tanstack-react-router-demo run build | Build production bundle |
pnpm --filter @xmachines/play-tanstack-react-router-demo run preview | Preview built bundle |
pnpm --filter @xmachines/play-tanstack-react-router-demo run test | Run Vitest test suite |
pnpm --filter @xmachines/play-tanstack-react-router-demo run test:browser | Run browser-focused Vitest suite |