Skip to content

@xmachines/play-solid-router-demo

Examples / @xmachines/play-solid-router-demo

SolidJS Router adapter demonstration with authentication flow using the XMachines Play architecture and provider-based router integration.

License: MIT

What This Demonstrates

  • Shared auth machine reused without framework-specific business logic
  • PlayRouterProvider renderer-based integration with Solid Router
  • Shell-driven rendering via PlayRenderer with actor-authoritative navigation
  • Canonical atoms lifecycle integration with Solid’s fine-grained reactivity
  • 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-solid-router-demo run dev

Then open http://localhost:3007.

Step-by-Step Code Flow

Use this order to understand the implementation:

  1. src/main.tsx bootstraps the demo and mounts <App /> onto #app.
  2. src/runtime.ts sets up the actor, registry, routes, and route map — all shared module-level singletons.
  3. src/App.tsx wires Solid Router routes and the PlayRouterProvider using the runtime exports.
  4. A routeMap is created by calling createRouteMap(authMachine) in src/runtime.ts.
  5. PlayRouterProvider forwards navigation intent from @solidjs/router to the actor, and reflects actor-approved routes back to the URL.
  6. Shell (from @xmachines/play-solid-demo) renders actor-projected views (PlayRenderer) and provides the shared UI shell, including the debug panel’s Show Inspector button.
  7. test/library-pattern.test.ts and test/browser/ verify invariants and runtime 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);
// src/App.tsx (shape)
import { Shell } from "@xmachines/play-solid-demo"; // shared demo shell (workspace-only package)
import { actor, inspector, registryResult, routeMap } from "./runtime.js";
const Layout: ParentComponent = () => {
const navigate = useNavigate();
const location = useLocation();
const params = useParams();
return (
<PlayRouterProvider
actor={actor}
routeMap={routeMap}
router={{ navigate, location, params }}
renderer={(currentActor, currentRouter) => (
<Shell
actor={currentActor}
router={currentRouter}
registryResult={registryResult}
inspector={inspector}
/>
)}
/>
);
};

Key Files

  • src/runtime.ts - actor lifecycle, registry, route extraction, and route map creation
  • src/App.tsx - Solid Router wiring, PlayRouterProvider integration, and renderer composition
  • 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

State Machine & Architecture Details

The demo utilizes XMachines architectural invariants:

  1. Actor Authority: When a user navigates to a protected route, the Solid 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., is the user logged in?) and transitions to the appropriate state.
  2. Passive Infrastructure: Solid components hold no business state. They observe actor.currentView to know what to render, and actor.currentRoute to reflect the active path.
  3. Atom-Only Reactivity: Instead of React-like re-renders, Solid uses createEffect internally within the RouterBridge to react precisely when atoms update.

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 updates Solid-local render triggers

One subscription lives for the whole watch, and no caller arms a watcher again. Cleanup is explicit and lifecycle-bound: Solid teardown uses onCleanup, and provider/bridge teardown must call disconnect/unwatch paths rather than relying on GC-only cleanup. This is crucial to prevent memory leaks during component unmounting.

Adapter Boundaries

Solid Router integration remains passive infrastructure. RouterBridgeBase stays the shared policy point; the Solid adapter only implements framework port behavior (calling navigate() and tracking location.pathname).

Available Scripts

These commands are defined in package.json:

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

Learn More