Vanilla JavaScript Router Demo
Examples / @xmachines/play-router-demo
Pure JavaScript integration proving Play Architecture works without any framework.
What This Demonstrates
This demo uses no framework - just vanilla JavaScript with the Browser History API and DOM manipulation. It proves that Play Architecture’s universal abstractions work at the lowest level, making framework integrations simpler by comparison.
Key Features
- Zero Framework Dependencies: Pure JavaScript and DOM
- Browser History API: Direct integration with
window.history - Same Auth Machine: Identical authentication logic as React/Vue/SolidJS demos
- Reference Implementation: This demo serves as the visual reference for all framework demos
Running the Demo
From the repository root:
npm installnpm run dev -w packages/play-router/examples/demoVisit http://localhost:5174.
Step-by-Step Code Flow
Use this order to understand the demo implementation:
src/main.tscreates the actor from the shared machine/catalog and starts it.src/router.tsextracts machine routes, builds browser-history routing primitives, and connects actor <-> router sync.src/shell.tssubscribes to actor signals and renders state-projected DOM views.- Navigation and auth interactions dispatch actor events (
play.route,auth.login,auth.logout) rather than mutating URL/state directly. - Browser tests in
test/browser/validate startup rendering and the auth route flow end-to-end.
// src/main.ts (shape)const actor = createActorFromSharedMachine();actor.start();
const cleanupRouter = initRouter(actor);const app = document.getElementById("app");if (app) { initShell(actor, app);}// src/router.ts (shape)const routeTree = extractMachineRoutes(machine);const routeMap = createRouteMap(routeTree);const history = createBrowserHistory();const router = createRouter({ history, routeMap });connectRouter({ actor, router, routeMap });// src/shell.ts (shape)actor.currentView.watch(() => { renderView(actor.currentView.get());});
loginButton.onclick = () => actor.send({ type: "auth.login", username, password });logoutButton.onclick = () => actor.send({ type: "auth.logout" });Key Files
src/main.ts- actor startup, router initialization, and shell bootstrapsrc/router.ts-createBrowserHistory,createRouter, andconnectRouterwiringsrc/shell.ts- DOM rendering and reactive watcher loop for state-driven updatestest/browser/startup.browser.test.ts- startup rendering assertion for public home + login actiontest/browser/auth-flow.browser.test.ts- login -> dashboard -> profile -> logout browser flow
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants at the lowest level:
- Actor Authority: Intercepting browser
popstateevents or link clicks dispatches aplay.routeevent to the actor. The actor processes the request against the current state, ensuring any auth logic is correctly applied. - Passive Infrastructure: The router simply updates
window.historyand the DOM solely renders HTML text based on whatactor.currentView.get()yields. - Signal-Only Reactivity: The integration registers manual
Signal.subtle.Watcherobservers that manually trigger DOM reconciliation, illustrating the underlying mechanism abstraction used by framework adapters.
Watcher Lifecycle and Cleanup Contract
Even in the vanilla demo, watcher handling follows the same canonical lifecycle:
notifyqueueMicrotaskgetPending()- Read actor signals and project DOM state
- Re-arm with
watch()/watch(...signals)
Watcher notifications are one-shot, so re-arm is mandatory. Router/shell teardown must explicitly call cleanup (disconnect/unwatch) and not rely on GC-only behavior. In vanilla JS, this means retaining the cleanup functions and calling them during hot-module replacement or app unmount.
Adapter Boundaries
connectRouter and browser-history wiring are passive infrastructure. Actor logic remains authoritative for route validity, and shared synchronization policy stays in RouterBridgeBase for framework bridges.
Why Vanilla?
Vanilla JavaScript is the foundation:
- Universal Patterns: If it works in vanilla, frameworks are just convenience layers
- No Magic: Every interaction is explicit - great for learning
- Performance Baseline: Shows the minimal overhead of Play Architecture
This demo proves frameworks aren’t required for Play Architecture to work.
Available Scripts
These commands are defined in package.json:
| Command | Description |
|---|---|
npm run dev -w packages/play-router/examples/demo | Start Vite dev server |
npm run build -w packages/play-router/examples/demo | Build production bundle |
npm run preview -w packages/play-router/examples/demo | Preview built bundle |
npm run test -w packages/play-router/examples/demo | Run Vitest test suite |
npm run test:browser -w packages/play-router/examples/demo | Run browser-focused Vitest suite |
Verification
Validate the operational behavior with package-local tests:
npm run test -w packages/play-router/examples/demonpm run test:browser -w packages/play-router/examples/demoExpected result: tests pass, including startup rendering and the full browser auth flow in test/browser/.
Learn More
- Play Router Package README - Router APIs and integration patterns