Skip to content

TanStack React Router Demo

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

React + TanStack Router demonstration of Play’s actor-authoritative routing and rendering model.

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

Running the Demo

From this directory (packages/play-tanstack-react-router/examples/demo):

Terminal window
npm install
npm run dev

Open http://localhost:3000.

Step-by-Step Code Flow

This demo uses provider-based integration centered on PlayRouterProvider and a route map generated from the machine:

  1. src/main.tsx mounts <App />.
  2. src/App.tsx creates/starts the actor from shared machine + catalog.
  3. src/App.tsx builds routeMap from extractMachineRoutes(authMachine) + createRouteMapFromTree(routeTree).
  4. PlayRouterProvider bridges TanStack Router to the actor and renders Shell via renderer(actor, router).
  5. Shell renders PlayRenderer, header/nav, and debug panel from actor state.
  6. Tests in test/ and test/browser*/ validate invariant and runtime behavior.
// src/App.tsx (shape)
const routeTree = extractMachineRoutes(authMachine);
const routeMap = createRouteMapFromTree(routeTree);
return (
<PlayRouterProvider
actor={actor}
router={router}
routeMap={routeMap}
renderer={(currentActor, currentRouter) => (
<Shell actor={currentActor} router={currentRouter} />
)}
/>
);
// src/components/Login.tsx (shape)
<button onClick={() => actor.send({ type: "auth.login", username })}>Login</button>

Shared business logic comes from the common demo machine/catalog module (authMachine and catalog), so routing decisions remain machine-driven and framework-independent.

Key Files

  • src/main.tsx - React entry point that mounts <App />
  • src/App.tsx - actor lifecycle, route-map creation, provider wiring, and Shell rendering
  • src/components/ - UI components that send actor events and render state-driven views
  • test/actor-authority.test.ts - actor authority and guarded navigation behavior
  • test/strict-separation.test.ts - machine/view infrastructure separation contracts
  • test/browser-e2e/auth-flow.browser.test.tsx - canonical browser auth flow
  • test/browser/ - extended browser checks for URL sync, history behavior, and route event flow

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. Signal-Only Reactivity: The bridge leverages React’s internal mechanisms combined with TanStack’s router.subscribe to react precisely when signals update, without relying on useEffect synchronization inside the application codebase.

Watcher Lifecycle and Cleanup Contract

This demo follows the canonical watcher lifecycle:

  1. notify
  2. queueMicrotask
  3. getPending()
  4. Read actor signals and trigger React state updates
  5. Re-arm with watch()/watch(...signals)

Watcher notifications are one-shot. 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

Terminal window
npm run dev # Start Vite dev server (http://localhost:3000)
npm run build # Build production assets
npm run preview # Preview production build locally
npm run test # Run Vitest suite
npm run test:vitest # Explicit Vitest command alias
npm run test:browser # Browser-focused test run
npm run test:browser:e2e # E2E-specific browser suite
npm run test:e2e # Alias to test:browser

Verification

Run the core invariant checks referenced by this demo docs:

Terminal window
npm run test:vitest -- test/actor-authority.test.ts test/strict-separation.test.ts
npm run test:browser

Manual sanity flow:

  1. Run npm run dev and open http://localhost:3000.
  2. Attempt protected navigation while logged out and confirm guard-driven behavior.
  3. Log in, then confirm route and view updates stay in sync.

Learn More