@xmachines/play-tanstack-solid-router-demo
Examples / @xmachines/play-tanstack-solid-router-demo
Solid + @tanstack/solid-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 - Object-based routing and deep location observation using TanStack’s Solid primitives
- 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-solid-router-demo run devThen open http://localhost:3012.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tsxbootstraps the demo app and mounts<App />onto#app.src/runtime.tsinitializes the actor, builds the registry, and creates the route map usingcreateRouteMap(authMachine).src/App.tsxconstructs the TanStack Router route tree from machine routes and wiresPlayRouterProviderwith the actor, router, and route map fromruntime.ts.- Router infrastructure stays passive: it forwards navigation intent to the actor via the
PlayRouterProviderand reflects actor-approved route changes back to TanStack. Shell(from@xmachines/play-solid-demo) renders actor-projected state (PlayRenderer), emits actor events via theregistryResultactions, and shows the debug panel’s Show Inspector button.test/library-pattern.test.tsplustest/browser/verify invariants and browser routing behavior.
// src/main.tsx (shape)render(() => <App />, document.getElementById("app")!);// src/runtime.ts (shape)export const inspector = createDemoInspector();const createPlayer = definePlayer({ machine: authMachine, options: { inspect: inspector.inspect }, actor: compose(PlayerActor, withRouting, withView),});export const actor = createPlayer();actor.start();
export const registryResult = defineRegistry(authCatalog, { components: demoComponents, actions: createAuthActionHandlers(actor), // login/logout/route + task events + form actions});
export const routeMap = createRouteMap(authMachine);const routeTree = extractMachineRoutes(authMachine);export const routes = getRoutableRoutes(routeTree);// src/App.tsx (shape)import { Shell } from "@xmachines/play-solid-demo"; // shared demo shell (workspace-only package)import { actor, inspector, registryResult, routeMap, routes } from "./runtime.js";
// router: the TanStack router built below in src/App.tsx from `routes`return ( <PlayRouterProvider actor={actor} router={router} routeMap={routeMap} renderer={(currentActor, currentRouter) => ( <Shell actor={currentActor} router={currentRouter} registryResult={registryResult} inspector={inspector} /> )} />);Key Files
src/runtime.ts- actor lifecycle, registry, route map, and routable routes setupsrc/App.tsx- TanStack router construction andPlayRouterProviderwiringsrc/main.tsx- Vite bootstrap that mounts<App />via Solid’srendertest/library-pattern.test.ts- architecture boundary and invariant assertionstest/browser/shared-demo.browser.test.ts- browser startup and full auth flow coveragetest/browser/xstate-route-events.browser.test.ts- actor route event behavior in the browser
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants:
- Actor Authority: When a user clicks a link, TanStack Router updates the location object. The
PlayRouterProviderintercepts this, translates it to aplay.routeevent, and sends it to the actor. The actor evaluates guards and applies 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 Solid’s
createEffectand TanStack’srouter.subscribeto observe location changes and atom updates without polling.
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 state updates
In Solid, this is largely abstracted by createEffect within the bridge adapter. Cleanup remains explicit: the provider executes bridge.disconnect() on unmount (onCleanup) to prevent ghost subscriptions, ensuring safe component teardown and hot module replacement.
Adapter Boundaries
The TanStack Solid adapter wraps TanStack’s router.navigate({ to }) and router.subscribe methods while delegating core synchronization policy to RouterBridgeBase. This keeps complex double-dispatch logic normalized across all frameworks.
Available Scripts
These commands are defined in package.json:
| Command | Description |
|---|---|
pnpm --filter @xmachines/play-tanstack-solid-router-demo run dev | Start Vite dev server (http://localhost:3012/) |
pnpm --filter @xmachines/play-tanstack-solid-router-demo run build | Build production bundle |
pnpm --filter @xmachines/play-tanstack-solid-router-demo run preview | Preview built bundle |
pnpm --filter @xmachines/play-tanstack-solid-router-demo run test | Run Vitest test suite |
pnpm --filter @xmachines/play-tanstack-solid-router-demo run test:browser | Run browser-focused Vitest suite |