Skip to content

@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.

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
  • 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:

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

Then open http://localhost:3012.

Step-by-Step Code Flow

Use this order to understand the implementation:

  1. src/main.tsx bootstraps the demo app and mounts <App /> onto #app.
  2. src/runtime.ts initializes the actor, builds the registry, and creates the route map using createRouteMap(authMachine).
  3. src/App.tsx constructs the TanStack Router route tree from machine routes and wires PlayRouterProvider with the actor, router, and route map from runtime.ts.
  4. Router infrastructure stays passive: it forwards navigation intent to the actor via the PlayRouterProvider and reflects actor-approved route changes back to TanStack.
  5. Shell (from @xmachines/play-solid-demo) renders actor-projected state (PlayRenderer), emits actor events via the registryResult actions, and shows the debug panel’s Show Inspector button.
  6. test/library-pattern.test.ts plus test/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 setup
  • src/App.tsx - TanStack router construction and PlayRouterProvider wiring
  • src/main.tsx - Vite bootstrap that mounts <App /> via Solid’s render
  • test/library-pattern.test.ts - architecture boundary and invariant assertions
  • test/browser/shared-demo.browser.test.ts - browser startup and full auth flow coverage
  • test/browser/xstate-route-events.browser.test.ts - actor route event behavior in the browser

State Machine & Architecture Details

The demo utilizes XMachines architectural invariants:

  1. Actor Authority: When a user clicks a link, TanStack Router updates the location object. The PlayRouterProvider intercepts this, translates it to a play.route event, and sends it to the actor. The actor evaluates guards and applies 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 Solid’s createEffect and TanStack’s router.subscribe to observe location changes and atom updates without polling.

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 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:

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

Learn More