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.

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

Running the Demo

From the repository root:

Terminal window
npm install
npm run dev -w @xmachines/play-tanstack-solid-router-demo

Then open http://localhost:3005.

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) and emits actor events via the registryResult actions.
  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)
const createPlayer = definePlayer({ machine: authMachine });
export const actor = createPlayer();
actor.start();
export const routeMap = createRouteMap(authMachine);
const routeTree = extractMachineRoutes(authMachine);
export const routes = getRoutableRoutes(routeTree);
// src/App.tsx (shape)
import { actor, registryResult, routeMap, routes } from "./runtime.js";
return (
<PlayRouterProvider
actor={actor}
router={router}
routeMap={routeMap}
renderer={(currentActor, currentRouter) => (
<Shell actor={currentActor} router={currentRouter} registryResult={registryResult} />
)}
/>
);
// src/runtime.ts — actor events wired via registryResult 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, ...params }),

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. Signal-Only Reactivity: The bridge leverages Solid’s createEffect and TanStack’s router.subscribe to observe location changes and signal updates without polling.

Watcher Lifecycle and Cleanup Contract

This demo follows the canonical watcher lifecycle:

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

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
npm run dev -w @xmachines/play-tanstack-solid-router-demoStart Vite dev server
npm run build -w @xmachines/play-tanstack-solid-router-demoBuild production bundle
npm run preview -w @xmachines/play-tanstack-solid-router-demoPreview built bundle
npm run test -w @xmachines/play-tanstack-solid-router-demoRun Vitest test suite
npm run test:browser -w @xmachines/play-tanstack-solid-router-demoRun browser-focused Vitest suite

Verification

Use these checks to validate README claims against the current demo implementation:

Terminal window
npm run test -w @xmachines/play-tanstack-solid-router-demo
npm run test:browser -w @xmachines/play-tanstack-solid-router-demo

Expected result: library-pattern invariant tests and the browser shared-demo suite both pass, confirming actor-driven route and view updates including protected route redirection.

Learn More