@xmachines/play-react-router-demo
Examples / @xmachines/play-react-router-demo
React Router v7 integration demo for the XMachines Play architecture.
What This Demonstrates
- Shared auth machine reused without framework-specific business logic
PlayRouterProviderrenderer-based integration with React Router- Shell-driven rendering via
PlayRendererwith actor-authoritative navigation - Canonical TC39 Signals lifecycle mapped to React’s rendering loop
- Browser E2E coverage via a shared demo browser suite
Running the Demo
From the repository root:
npm installnpm run dev -w @xmachines/play-react-router-demoThen open http://localhost:5173.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tsxmounts<App />.src/App.tsxcallsuseMemo(createAppRuntime, [])to create the actor and browser router, starting the actor once per mounted app.src/runtime.tspre-builds therouteMap(viacreateRouteMap(authMachine)) and exportscreatePlayer,routeMap, andcreateRegistryResult— keeping runtime setup separate from React lifecycle concerns.createAppRuntime()creates the actor, builds aregistryResultfrom the registry, and constructs a React Router data router with aRoutedShellwrapper.PlayRouterProviderwires actor navigation and router changes in both directions, using the pre-builtrouteMap.Shell(insideApp.tsx) receivesactor,router, andregistryResult, matching the other router demos.- The
rendererprop renders<RouterProvider router={currentRouter} />directly — no intermediateReactRouterAppwrapper is needed. - Browser tests in
test/browser/validate startup and auth navigation flow via a shared demo suite.
// src/main.tsx (shape)createRoot(document.getElementById("root")!).render(<App />);// src/runtime.ts (shape)export const createPlayer = definePlayer({ machine: authMachine });export const routeMap = createRouteMap(authMachine);
export function createRegistryResult(actor: ReturnType<typeof createPlayer>) { return defineRegistry(authCatalog, { components: { Home, About, Contact, Login, Dashboard /* … */ }, 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 }), }, });}// src/App.tsx (shape)function createAppRuntime() { const actor = createPlayer(); actor.start();
const registryResult = createRegistryResult(actor);
let router!: BrowserRouterInstance;
function RoutedShell() { return <Shell actor={actor} router={router} registryResult={registryResult} />; }
router = createBrowserRouter([{ path: "*", element: <RoutedShell /> }]);
return { actor, router };}
const { actor, router } = useMemo(createAppRuntime, []);
return ( <PlayRouterProvider actor={actor} router={router} routeMap={routeMap} renderer={(_, currentRouter) => <RouterProvider router={currentRouter} />} />);Key Files
src/main.tsx- React entry point that mounts<App />src/runtime.ts- actor factory (createPlayer), pre-builtrouteMap, andcreateRegistryResultregistry wiringsrc/App.tsx- actor lifecycle, provider wiring, andRoutedShellcompositiontest/browser/shared-demo.browser.test.tsx- browser E2E suite for startup and auth navigation flow
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants:
- Actor Authority: Navigation triggers URL changes, which the
PlayRouterProviderintercepts and converts intoplay.routeevents. The actor evaluates these events against its internal guards and transitions. - Passive Infrastructure: The router does not execute business logic. The actor dictates whether navigation is permitted. The React application only triggers renders.
- Signal-Only Reactivity: The bridge leverages React’s
useSyncExternalStore(internally within the hooks) to react precisely when signals update, without polluting the React component tree withuseStateoruseEffectfor business logic.
Watcher Lifecycle and Cleanup Contract
This demo follows the same watcher lifecycle used across @xmachines/play-signals and framework adapters:
notifyqueueMicrotask- Drain pending work with
getPending() - Read actor signals and project framework-local state
- Re-arm via
watch()/watch(...signals)
Watcher notifications are one-shot. Cleanup is explicit: bridge/provider teardown must call disconnect/unwatch paths, never rely on GC-only cleanup. The PlayRouterProvider handles this natively on component unmount.
Adapter Boundaries
PlayRouterProvider and the React Router bridge stay passive infrastructure. Business validity remains actor-owned, while RouterBridgeBase remains the shared policy layer and the concrete React adapter stays a thin port, delegating DOM synchronization to React Router. The demo keeps the same Shell(actor, router, registryResult) shape as the other router demos, with RouterProvider rendered directly inside the renderer callback — no additional wrapper component is required.
Available Scripts
These commands are defined in package.json:
| Command | Description |
|---|---|
npm run dev -w @xmachines/play-react-router-demo | Start Vite dev server |
npm run build -w @xmachines/play-react-router-demo | Build production bundle |
npm run preview -w @xmachines/play-react-router-demo | Preview built bundle |
npm run test -w @xmachines/play-react-router-demo | Run Vitest test suite |
npm run test:browser -w @xmachines/play-react-router-demo | Run browser-focused Vitest suite |
Verification
Use these checks to validate README claims against the current demo implementation:
npm run test -w @xmachines/play-react-router-demonpm run test:browser -w @xmachines/play-react-router-demoExpected result: tests pass for startup and auth-flow browser scenarios.