@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
Running the Demo
From the repository root:
npm installnpm run dev -w @xmachines/play-tanstack-react-router-demoThen open http://localhost:3000.
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.tsexportscreatePlayer(viadefinePlayer),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).ShellrendersPlayRenderer, header/nav, and debug panel from actor state.- Tests in
test/andtest/browser/validate invariant and runtime behavior.
// src/App.tsx (shape)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} /> )} />);// src/runtime.ts (shape)export const createPlayer = definePlayer({ machine: authMachine });export const routeMap = createRouteMap(authMachine);
export function createRegistryResult(actor) { return defineRegistry(authCatalog, { actions: { login: async (args) => actor.send({ type: "auth.login", username: args.username }), logout: async () => actor.send({ type: "auth.logout" }), route: async (args) => actor.send({ type: "play.route", to: args.to, ...args.params }), }, });}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/signal-only.test.ts- signal-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.
- Signal-Only Reactivity: The bridge leverages React’s internal mechanisms combined with TanStack’s
router.subscribeto react precisely when signals update, without relying onuseEffectsynchronization inside the application codebase.
Watcher Lifecycle and Cleanup Contract
This demo follows the canonical watcher lifecycle:
notifyqueueMicrotaskgetPending()- Read actor signals and trigger React state updates
- 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
These commands are defined in package.json:
| Command | Description |
|---|---|
npm run dev -w @xmachines/play-tanstack-react-router-demo | Start Vite dev server |
npm run build -w @xmachines/play-tanstack-react-router-demo | Build production bundle |
npm run preview -w @xmachines/play-tanstack-react-router-demo | Preview built bundle |
npm run test -w @xmachines/play-tanstack-react-router-demo | Run Vitest test suite |
npm run test:browser -w @xmachines/play-tanstack-react-router-demo | Run browser-focused Vitest suite |
Verification
Use these checks to validate README claims against the current demo implementation:
npm run test -w @xmachines/play-tanstack-react-router-demonpm run test:browser -w @xmachines/play-tanstack-react-router-demoExpected result: invariant test suite and the browser shared-demo suite both pass, confirming actor authority, route sync, and guard-driven redirection.