@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.
What This Demonstrates
- Shared auth machine reused without framework-specific business logic
PlayRouterProviderrenderer-based integration with Solid Router- Shell-driven rendering via
PlayRendererwith 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:
pnpm installpnpm --filter @xmachines/play-solid-router-demo run devThen open http://localhost:3007.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tsxbootstraps the demo and mounts<App />onto#app.src/runtime.tssets up the actor, registry, routes, and route map — all shared module-level singletons.src/App.tsxwires Solid Router routes and thePlayRouterProviderusing the runtime exports.- A
routeMapis created by callingcreateRouteMap(authMachine)insrc/runtime.ts. PlayRouterProviderforwards navigation intent from@solidjs/routerto the actor, and reflects actor-approved routes back to the URL.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.test/library-pattern.test.tsandtest/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 creationsrc/App.tsx- Solid Router wiring,PlayRouterProviderintegration, and renderer compositionsrc/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 coverage
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants:
- Actor Authority: When a user navigates to a protected route, the Solid Router updates the URL. The
PlayRouterProviderintercepts this, translates it to aplay.routeevent, and sends it to the actor. The actor evaluates guards (e.g., is the user logged in?) and transitions to the appropriate state. - Passive Infrastructure: Solid components hold no business state. They observe
actor.currentViewto know what to render, andactor.currentRouteto reflect the active path. - Atom-Only Reactivity: Instead of React-like re-renders, Solid uses
createEffectinternally within theRouterBridgeto react precisely when atoms update.
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 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:
| Command | Description |
|---|---|
pnpm --filter @xmachines/play-solid-router-demo run dev | Start Vite dev server (http://localhost:3007/) |
pnpm --filter @xmachines/play-solid-router-demo run build | Build production bundle |
pnpm --filter @xmachines/play-solid-router-demo run preview | Preview built bundle |
pnpm --filter @xmachines/play-solid-router-demo run test | Run Vitest test suite |
pnpm --filter @xmachines/play-solid-router-demo run test:browser | Run browser-focused Vitest suite |