Skip to content

@xmachines/play-react-demo

Examples / @xmachines/play-react-demo

React renderer demo for @xmachines/play-react — actor + PlayRenderer without a router.

What This Demonstrates

  • Shared auth machine reused without framework-specific business logic
  • PlayRenderer rendering actor-projected views with a typed defineRegistry catalog
  • Auth machine states drive view switching via auth.login / auth.logout events only — no play.route routing
  • Canonical TC39 Signals lifecycle surfaced through React’s rendering model
  • Non-browser invariant tests plus browser renderer coverage

Running the Demo

From the repository root:

Terminal window
npm install
npm run dev -w @xmachines/play-react-demo

Then open http://localhost:5173.

Step-by-Step Code Flow

Use this order to understand the implementation:

  1. src/main.tsx mounts <App />.
  2. src/App.tsx creates the actor at module scope via definePlayer({ machine: authMachine })() — created once when the module is first evaluated.
  3. defineRegistry(authCatalog, { components, actions }) builds the typed registryResult — real async action handlers dispatching to actor.send().
  4. <PlayUIProvider actor={actor} registryResult={registryResult}><PlayRenderer /></PlayUIProvider> observes actor.currentView and renders the active spec.
  5. A NavBar component observes actor signals directly for nav visibility.
  6. <DebugPanel actor={actor} /> shows live state, auth status, and current route.
  7. Browser tests in test/browser/ validate startup and interaction behavior.
// src/main.tsx (shape)
createRoot(document.getElementById("root")!).render(<App />);
// src/App.tsx (shape)
// Both actor and registryResult are module-scope singletons — created once,
// never recreated, no useMemo needed inside the component.
const actor: AuthActor = definePlayer({ machine: authMachine })();
actor.start();
const registryResult = defineRegistry(authCatalog, {
components: {
Home,
About,
Contact,
Login,
Dashboard,
Overview,
Stats,
Profile,
Settings,
Navigation,
NavBar: NavBarView,
},
actions: {
login: async (args) =>
actor.send({
type: "auth.login",
username: assertNonNullable(args, "args").username,
}),
logout: async () => actor.send({ type: "auth.logout" }),
route: async (args) => {
const { to, params } = assertNonNullable(args, "args");
actor.send({
type: "play.route",
to,
...(params != null && { params }),
});
},
},
});
export function App() {
return (
<div className="demo-app" data-demo-shell>
<header className="demo-header">
<h1 className="demo-title">XMachines Play React Demo</h1>
<NavBar actor={actor} />
</header>
<main className="demo-content" data-demo-content>
<PlayUIProvider actor={actor} registryResult={registryResult}>
<PlayRenderer />
</PlayUIProvider>
</main>
<DebugPanel actor={actor} />
</div>
);
}

Key Files

  • src/main.tsx - React entry point that mounts <App />
  • src/App.tsx - actor lifecycle, registry construction, and PlayUIProvider + PlayRenderer composition
  • src/catalog.ts - catalog definition using defineCatalog from @json-render/core with @json-render/react/schema
  • src/components/ - demo view components bound to catalog component keys (Home, Login, Dashboard, Profile, etc.)
  • test/library-pattern.test.ts - architecture boundary and invariant assertions
  • test/browser/renderer-demo.browser.test.tsx - browser-mode renderer coverage

State Machine & Architecture Details

The demo utilizes XMachines architectural invariants:

  1. Actor Authority: View components dispatch auth.login and auth.logout events to the actor. The actor evaluates guards and transitions — React never decides which view to render.
  2. Passive Infrastructure: PlayRenderer observes actor.currentView signals only. It holds no business state and makes no routing decisions.
  3. Signal-Only Reactivity: The bridge leverages React’s rendering model to react precisely when actor signals update, without polluting the component tree with useState or useEffect for business logic.

Watcher Lifecycle and Cleanup Contract

This demo follows the canonical watcher lifecycle used across all @xmachines framework adapters:

  1. notify
  2. queueMicrotask
  3. getPending()
  4. Read actor signals and project React render state
  5. Re-arm with watch()/watch(...signals)

Watcher notifications are one-shot. Cleanup is explicit: actor.stop() is called on component unmount via useEffect, and PlayRenderer handles internal watcher teardown natively.

Adapter Boundaries

PlayRenderer and defineRegistry stay passive infrastructure. Business validity remains actor-owned. The registry maps catalog component keys to React component implementations — the actor owns which key is active. The demo intentionally omits URL routing to isolate the renderer contract.

Available Scripts

These commands are defined in package.json:

CommandDescription
npm run dev -w @xmachines/play-react-demoStart Vite dev server
npm run build -w @xmachines/play-react-demoBuild production bundle
npm run preview -w @xmachines/play-react-demoPreview built bundle
npm run test -w @xmachines/play-react-demoRun Vitest test suite
npm run test:browser -w @xmachines/play-react-demoRun browser-focused Vitest suite

Verification

Use these checks to validate README claims against the current demo implementation:

Terminal window
npm run test -w @xmachines/play-react-demo
npm run test:browser -w @xmachines/play-react-demo

Expected result: library-pattern invariant tests pass and the browser renderer suite completes.

Learn More

Type Aliases

Variables

Functions