@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
PlayRendererrendering actor-projected views with a typeddefineRegistrycatalog- Auth machine states drive view switching via
auth.login/auth.logoutevents only — noplay.routerouting - 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:
npm installnpm run dev -w @xmachines/play-react-demoThen open http://localhost:5173.
Step-by-Step Code Flow
Use this order to understand the implementation:
src/main.tsxmounts<App />.src/App.tsxcreates the actor at module scope viadefinePlayer({ machine: authMachine })()— created once when the module is first evaluated.defineRegistry(authCatalog, { components, actions })builds the typedregistryResult— real async action handlers dispatching toactor.send().<PlayUIProvider actor={actor} registryResult={registryResult}><PlayRenderer /></PlayUIProvider>observesactor.currentViewand renders the active spec.- A
NavBarcomponent observesactorsignals directly for nav visibility. <DebugPanel actor={actor} />shows live state, auth status, and current route.- 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, andPlayUIProvider+PlayRenderercompositionsrc/catalog.ts- catalog definition usingdefineCatalogfrom@json-render/corewith@json-render/react/schemasrc/components/- demo view components bound to catalog component keys (Home, Login, Dashboard, Profile, etc.)test/library-pattern.test.ts- architecture boundary and invariant assertionstest/browser/renderer-demo.browser.test.tsx- browser-mode renderer coverage
State Machine & Architecture Details
The demo utilizes XMachines architectural invariants:
- Actor Authority: View components dispatch
auth.loginandauth.logoutevents to the actor. The actor evaluates guards and transitions — React never decides which view to render. - Passive Infrastructure:
PlayRendererobservesactor.currentViewsignals only. It holds no business state and makes no routing decisions. - Signal-Only Reactivity: The bridge leverages React’s rendering model to react precisely when actor signals update, without polluting the component tree with
useStateoruseEffectfor business logic.
Watcher Lifecycle and Cleanup Contract
This demo follows the canonical watcher lifecycle used across all @xmachines framework adapters:
notifyqueueMicrotaskgetPending()- Read actor signals and project React render state
- 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:
| Command | Description |
|---|---|
npm run dev -w @xmachines/play-react-demo | Start Vite dev server |
npm run build -w @xmachines/play-react-demo | Build production bundle |
npm run preview -w @xmachines/play-react-demo | Preview built bundle |
npm run test -w @xmachines/play-react-demo | Run Vitest test suite |
npm run test:browser -w @xmachines/play-react-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-demonpm run test:browser -w @xmachines/play-react-demoExpected result: library-pattern invariant tests pass and the browser renderer suite completes.
Learn More
Type Aliases
Variables
- About
- authCatalog
- Contact
- Dashboard
- DebugPanel
- Home
- Login
- NavBar
- NavBarView
- Navigation
- Overview
- Profile
- Settings
- Shell
- Stats