@xmachines/play-react-router-demo
Examples / @xmachines/play-react-router-demo
React Router 7/8 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 atoms lifecycle mapped to React’s rendering loop
- Browser E2E coverage via a shared demo browser suite
- 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-react-router-demo run devThen open http://localhost:3006.
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(imported from the workspace-only@xmachines/play-react-demo) receivesactor,router,registryResult, andinspector, 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 inspector = createDemoInspector();export const createPlayer = definePlayer({ machine: authMachine, options: { inspect: inspector.inspect }, actor: compose(PlayerActor, withRouting, withView),});export const routeMap = createRouteMap(authMachine);
export function createRegistryResult(actor: ReturnType<typeof createPlayer>) { return defineRegistry(authCatalog, { components: demoComponents, // from @xmachines/play-react-demo actions: createAuthActionHandlers(actor), });}// src/App.tsx (shape)import { Shell } from "@xmachines/play-react-demo"; // shared demo shell (workspace-only package)
type BrowserRouterInstance = ReturnType<typeof createBrowserRouter>;
function createAppRuntime() { const actor = createPlayer(); actor.start();
const registryResult = createRegistryResult(actor);
let router!: BrowserRouterInstance;
function RoutedShell() { return ( <Shell actor={actor} router={router} registryResult={registryResult} inspector={inspector} /> ); }
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.
- Atom-Only Reactivity: The bridge leverages React’s
useSyncExternalStore(internally within the hooks) to react precisely when atoms 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-atom and framework adapters:
- 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 projects framework-local state
One subscription lives for the whole watch, and no caller arms a watcher again. 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 |
|---|---|
pnpm --filter @xmachines/play-react-router-demo run dev | Start Vite dev server (http://localhost:3006/) |
pnpm --filter @xmachines/play-react-router-demo run build | Build production bundle |
pnpm --filter @xmachines/play-react-router-demo run preview | Preview built bundle |
pnpm --filter @xmachines/play-react-router-demo run test | Run Vitest test suite |
pnpm --filter @xmachines/play-react-router-demo run test:browser | Run browser-focused Vitest suite |