@xmachines/play-react 1.0.0-beta.3 → 1.0.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikael Karon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,358 +1,326 @@
1
1
  # @xmachines/play-react
2
2
 
3
- **React renderer consuming signals and UI schema with provider pattern**
3
+ **React renderer for XMachines Play Architecture**
4
4
 
5
- Signal-driven React rendering layer observing actor state with zero React state for business logic.
5
+ Bridges TC39 Signal-driven actors to React's render cycle. Business logic stays in the actor; React is purely a rendering target.
6
6
 
7
7
  ## Overview
8
8
 
9
- `@xmachines/play-react` provides `PlayRenderer` and `useSignalEffect` for building React UIs that passively observe actor signals. This package enables framework-swappable architecture where React is just a rendering target subscribing to signal changes — business logic lives entirely in the actor.
9
+ `@xmachines/play-react` provides `PlayRenderer`, a React component that:
10
10
 
11
- Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md), this package implements:
11
+ - Subscribes to `actor.currentView` (TC39 Signal) and re-renders on every state transition
12
+ - Renders the current view's JSON spec via `@json-render/react`
13
+ - Routes action names from spec elements to `actor.send()` via the `actions` prop
14
+ - Manages per-view UI state in an `@xstate/store` atom (automatic or caller-supplied)
12
15
 
13
- - **Signal-Only Reactivity (INV-05):** No useState/useReducer for business logic, signals only
14
- - **Passive Infrastructure (INV-04):** Components observe signals, send events to actor
16
+ Per [Play RFC](../docs/rfc/play.md):
15
17
 
16
- **Key Principle:** React state is never used for business logic. Signals are the source of truth.
17
-
18
- Renderer receives actor via props (provider pattern), not children.
18
+ - **Actor Authority (INV-01):** Guards in the machine decide all state transitions
19
+ - **Passive Infrastructure (INV-04):** React observes signals and dispatches events — never decides
20
+ - **Signal-Only Reactivity (INV-05):** `actor.currentView` signal is the sole render trigger
19
21
 
20
22
  ## Installation
21
23
 
22
24
  ```bash
23
- npm install react@^18.0.0 react-dom@^18.0.0
24
25
  npm install @xmachines/play-react
26
+ npm install @json-render/react @json-render/core # peer deps
27
+ npm install @json-render/xstate @xstate/store # store integration
25
28
  ```
26
29
 
30
+ In this monorepo, the root install applies a `patch-package` patch to `@json-render/react`
31
+ so `defineRegistry(..., { onRenderError })` can intercept inner element-boundary errors
32
+ without muting console output.
33
+
27
34
  ## Current Exports
28
35
 
29
- - `PlayRenderer`
30
- - `useSignalEffect`
36
+ - `PlayRenderer` — main renderer component
37
+ - `useActor` — hook for accessing the actor inside a `PlayRenderer` tree
38
+ - `useSignalEffect` — React hook for subscribing to TC39 Signals
39
+ - `PlayErrorBoundary` — error boundary wrapping renderer output
40
+ - `defineRegistry` — re-exported from `@json-render/react`
41
+ - `useStateBinding` — re-exported from `@json-render/react`
42
+ - `ComponentFn` (type) — re-exported from `@json-render/react`
43
+ - `ComponentContext` (type) — re-exported from `@json-render/react`
31
44
  - `PlayRendererProps` (type)
32
-
33
- **Peer dependencies:**
34
-
35
- - `react` ^18.0.0 - React runtime
36
- - `react-dom` ^18.0.0 - React DOM renderer
45
+ - `PlayActor` (type)
37
46
 
38
47
  ## Quick Start
39
48
 
40
- ```typescript
41
- import { createRoot } from "react-dom/client";
42
- import { definePlayer } from "@xmachines/play-xstate";
43
- import { defineCatalog } from "@xmachines/play-catalog";
49
+ ```tsx
50
+ import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
44
51
  import { PlayRenderer } from "@xmachines/play-react";
52
+ import { defineCatalog } from "@json-render/core";
53
+ import { defineRegistry } from "@xmachines/play-react";
54
+ import type { ComponentFn } from "@xmachines/play-react";
55
+ import { setup, assign } from "xstate";
45
56
  import { z } from "zod";
46
57
 
47
- // 1. Define catalog (business logic layer)
58
+ // 1. Define catalog the contract between machine spec and UI components
48
59
  const catalog = defineCatalog({
49
- LoginForm: z.object({ error: z.string().optional() }),
50
- Dashboard: z.object({
51
- userId: z.string(),
52
- username: z.string(),
53
- }),
60
+ elements: {
61
+ Login: { props: z.object({ title: z.string() }), description: "Login form" },
62
+ Dashboard: { props: z.object({ username: z.string() }), description: "Dashboard" },
63
+ },
64
+ });
65
+
66
+ // 2. Implement components using ComponentFn — typed against catalog entries
67
+ const Login: ComponentFn<typeof catalog, "Login"> = ({ props, emit }) => (
68
+ <form
69
+ onSubmit={(e) => {
70
+ e.preventDefault();
71
+ emit("submit");
72
+ }}
73
+ >
74
+ <h2>{props.title}</h2>
75
+ <button type="submit">Log In</button>
76
+ </form>
77
+ );
78
+
79
+ const Dashboard: ComponentFn<typeof catalog, "Dashboard"> = ({ props }) => (
80
+ <div>Welcome, {props.username}!</div>
81
+ );
82
+
83
+ // 3. Build registry — wires components to catalog and declares no-op action stubs
84
+ const { registry } = defineRegistry(catalog, {
85
+ components: { Login, Dashboard },
86
+ actions: { login: async () => {}, logout: async () => {} },
54
87
  });
55
88
 
56
- // 2. Create React components (view layer)
57
- const components = {
58
- LoginForm: ({ error, send }) => (
59
- <form
60
- onSubmit={(e) => {
61
- e.preventDefault();
62
- const data = new FormData(e.currentTarget);
63
- send({
64
- type: "auth.login",
65
- username: data.get("username"),
66
- password: data.get("password"),
67
- });
68
- }}
69
- >
70
- {error && <p style={{ color: "red" }}>{error}</p>}
71
- <input name="username" required placeholder="Username" />
72
- <input name="password" type="password" required placeholder="Password" />
73
- <button type="submit">Log In</button>
74
- </form>
75
- ),
76
- Dashboard: ({ userId, username, send }) => (
77
- <div>
78
- <h1>Welcome, {username}!</h1>
79
- <p>User ID: {userId}</p>
80
- <button onClick={() => send({ type: "auth.logout" })}>Log Out</button>
81
- </div>
82
- ),
83
- };
84
-
85
- // 3. Create player actor (business logic runtime)
86
- const createPlayer = definePlayer({ machine: authMachine, catalog });
89
+ // 4. Define machine with view metadata
90
+ const machine = setup({
91
+ types: {
92
+ context: {} as {
93
+ isAuthenticated: boolean;
94
+ username: string | null;
95
+ params: Record<string, string>;
96
+ query: Record<string, string>;
97
+ },
98
+ events: {} as
99
+ | { type: "auth.login"; username: string }
100
+ | { type: "auth.logout" }
101
+ | { type: "play.route"; to: string; params?: Record<string, string> },
102
+ },
103
+ }).createMachine(
104
+ formatPlayRouteTransitions({
105
+ id: "app",
106
+ initial: "login",
107
+ context: { isAuthenticated: false, username: null, params: {}, query: {} },
108
+ states: {
109
+ login: {
110
+ id: "login",
111
+ meta: {
112
+ route: "/login",
113
+ view: {
114
+ component: "Login",
115
+ spec: {
116
+ root: "root",
117
+ elements: {
118
+ root: { type: "Login", props: { title: "Sign In" }, children: [] },
119
+ },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ dashboard: {
125
+ id: "dashboard",
126
+ meta: {
127
+ route: "/dashboard",
128
+ view: {
129
+ component: "Dashboard",
130
+ spec: {
131
+ root: "root",
132
+ elements: {
133
+ root: { type: "Dashboard", props: { username: "" }, children: [] },
134
+ },
135
+ },
136
+ },
137
+ },
138
+ },
139
+ },
140
+ on: {
141
+ "auth.login": {
142
+ target: ".dashboard",
143
+ guard: ({ context }) => !context.isAuthenticated,
144
+ actions: assign({
145
+ isAuthenticated: true,
146
+ username: ({ event }) => event.username,
147
+ }),
148
+ },
149
+ "auth.logout": {
150
+ target: ".login",
151
+ guard: ({ context }) => context.isAuthenticated,
152
+ actions: assign({ isAuthenticated: false, username: null }),
153
+ },
154
+ },
155
+ }),
156
+ );
157
+
158
+ // 5. Create actor and render
159
+ const createPlayer = definePlayer({ machine });
87
160
  const actor = createPlayer();
88
161
  actor.start();
89
162
 
90
- // 4. Render UI (actor via props)
91
- const root = createRoot(document.getElementById("app")!);
92
- root.render(<PlayRenderer actor={actor} components={components} />);
163
+ function App() {
164
+ return (
165
+ <PlayRenderer
166
+ actor={actor}
167
+ registry={registry}
168
+ actions={{ login: "auth.login", logout: "auth.logout" }}
169
+ />
170
+ );
171
+ }
93
172
  ```
94
173
 
95
174
  ## API Reference
96
175
 
97
- ### PlayRenderer
176
+ ### `PlayRenderer`
98
177
 
99
- Main renderer component subscribing to actor signals and dynamically rendering catalog components:
178
+ Main component. Subscribes to `actor.currentView` and renders the spec.
100
179
 
101
- ```typescript
102
- interface PlayRendererProps {
103
- actor: AbstractActor<any>;
104
- components: Record<string, React.ComponentType<any>>;
105
- fallback?: React.ReactNode;
106
- }
180
+ ```tsx
181
+ <PlayRenderer
182
+ actor={actor} // required
183
+ registry={registry} // required
184
+ actions={{ login: "auth.login" }} // optional
185
+ store={myStore} // optional — controlled mode
186
+ fallback={<p>Loading…</p>} // optional
187
+ />
107
188
  ```
108
189
 
109
- **Props:**
110
-
111
- - `actor` - Actor instance with `currentView` signal
112
- - `components` - Map of component names to React components
113
- - `fallback` - Component shown when `currentView` is null (default: `null`)
190
+ **`actor`** — A `PlayerActor` (or any `AbstractActor & Viewable`). Provides the `currentView` signal.
114
191
 
115
- **Behavior:**
192
+ **`registry`** — Built with `defineRegistry(catalog, { components, actions })` from `@xmachines/play-react`.
116
193
 
117
- 1. Subscribes to `actor.currentView` signal via `useSignalEffect`
118
- 2. Looks up component from `components` map using `view.component` string
119
- 3. Renders component with props from `view.props` + `send` function
194
+ `defineRegistry` also accepts `onRenderError(error, elementType)`, which receives errors
195
+ caught by `@json-render/react`'s inner element boundary before the default logger is used.
120
196
 
121
- **Example:**
197
+ **`actions`** — Maps json-render action names (from spec `on` bindings) to XState event type strings. Values are type-checked against `EventFromLogic<TLogic>["type"]` when `TLogic` is specified:
122
198
 
123
- ```typescript
124
- <PlayRenderer
199
+ ```tsx
200
+ // Typed: "bad.event" causes a compile error if it is not in the machine's event union
201
+ <PlayRenderer<typeof machine>
125
202
  actor={actor}
126
- components={{
127
- HomePage: ({ send }) => <div>Home</div>,
128
- AboutPage: ({ send }) => <div>About</div>,
129
- }}
130
- fallback={<div>Loading...</div>}
203
+ registry={registry}
204
+ actions={{ login: "auth.login", logout: "auth.logout" }}
131
205
  />
132
206
  ```
133
207
 
134
- ### useSignalEffect()
208
+ **`store`** (optional) — Controls per-view UI state (form values, `$state` bindings):
135
209
 
136
- Hook for subscribing to signal changes with automatic cleanup:
210
+ - **Omitted (uncontrolled, default):** A fresh `@xstate/store` atom is created per view transition, seeded from `view.spec.state`. The atom resets automatically on each state transition.
211
+ - **Provided (controlled):** The caller owns the store lifecycle. `spec.state` is ignored.
137
212
 
138
- ```typescript
139
- useSignalEffect(() => {
140
- const value = signal.get();
141
- // React re-renders when signal changes
142
- });
143
- ```
213
+ ```tsx
214
+ import { createAtom } from "@xstate/store";
215
+ import { xstateStoreStateStore } from "@json-render/xstate";
216
+ import type { StateStore } from "@json-render/core";
144
217
 
145
- **Behavior:**
218
+ const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "alice" }) });
146
219
 
147
- - Tracks signal dependencies automatically via `Signal.Computed` wrapper
148
- - Uses `Signal.subtle.Watcher` with microtask batching
149
- - Triggers React state update to force re-render
150
- - Cleans up watcher on unmount with explicit `unwatch`
220
+ <PlayRenderer actor={actor} registry={registry} store={store} actions={{ login: "auth.login" }} />;
221
+ ```
151
222
 
152
- **Canonical watcher lifecycle:**
223
+ **Inner render errors** — You can intercept catalog component render failures without
224
+ overriding the outer `PlayErrorBoundary`:
153
225
 
154
- 1. `notify`
155
- 2. `queueMicrotask`
156
- 3. drain pending work (`getPending` and/or `Computed.get`)
157
- 4. run effect + trigger render
158
- 5. re-arm watcher via `watch()`
226
+ ```tsx
227
+ const { registry } = defineRegistry(catalog, {
228
+ components: { Login, Dashboard },
229
+ actions: { login: async () => {}, logout: async () => {} },
230
+ onRenderError(error, elementType) {
231
+ reportExpectedRenderError(error, elementType);
232
+ },
233
+ });
234
+ ```
159
235
 
160
- Watcher notification is one-shot, so re-arm and explicit cleanup are both required.
236
+ **`fallback`** Shown when `actor.currentView.get()` is `null` (machine in a no-view state, or during initialisation).
161
237
 
162
- **Example:**
238
+ ---
163
239
 
164
- ```typescript
165
- import { useSignalEffect } from "@xmachines/play-react";
166
- import { useState } from "react";
240
+ ### `useActor`
167
241
 
168
- function RouteDisplay({ actor }: { actor: AbstractActor<any> }) {
169
- const [route, setRoute] = useState<string | null>(null);
242
+ Hook for accessing the actor from inside any component rendered by `PlayRenderer`. No prop drilling needed.
170
243
 
171
- useSignalEffect(() => {
172
- const currentRoute = actor.currentRoute.get();
173
- setRoute(currentRoute);
174
- });
244
+ ```tsx
245
+ import { useActor } from "@xmachines/play-react";
175
246
 
176
- return <div>Current Route: {route ?? "None"}</div>;
247
+ // Works in any component rendered inside PlayRenderer:
248
+ function LogoutButton() {
249
+ const actor = useActor();
250
+ return <button onClick={() => actor.send({ type: "auth.logout" })}>Log Out</button>;
177
251
  }
178
252
  ```
179
253
 
180
- **Complete API:** See [API Documentation](../../docs/api/@xmachines/play-react)
181
-
182
- ## Examples
254
+ Throws `"useActor() must be called inside <PlayRenderer>"` if called outside the tree.
183
255
 
184
- ### Component Receiving Props from Catalog
256
+ ---
185
257
 
186
- ```typescript
187
- import { PlayRenderer } from "@xmachines/play-react";
188
- import { defineCatalog } from "@xmachines/play-catalog";
189
- import { z } from "zod";
190
-
191
- // Define schema in catalog
192
- const catalog = defineCatalog({
193
- UserProfile: z.object({
194
- userId: z.string(),
195
- name: z.string(),
196
- avatar: z.string().url().optional(),
197
- stats: z.object({
198
- posts: z.number(),
199
- followers: z.number(),
200
- }),
201
- }),
202
- });
203
-
204
- // Component receives type-safe props + send
205
- const components = {
206
- UserProfile: ({ userId, name, avatar, stats, send }) => (
207
- <div>
208
- {avatar && <img src={avatar} alt={name} />}
209
- <h1>{name}</h1>
210
- <p>ID: {userId}</p>
211
- <div>
212
- <span>{stats.posts} posts</span>
213
- <span>{stats.followers} followers</span>
214
- </div>
215
- <button
216
- onClick={() =>
217
- send({
218
- type: "profile.edit",
219
- userId,
220
- })
221
- }
222
- >
223
- Edit Profile
224
- </button>
225
- </div>
226
- ),
227
- };
228
-
229
- <PlayRenderer actor={actor} components={components} />;
230
- ```
258
+ ### `useSignalEffect`
231
259
 
232
- ### useSignalEffect for Custom Rendering
260
+ Hook for subscribing to TC39 Signals in React components that live outside a `PlayRenderer` tree (e.g. nav bars, status indicators driven by actor state).
233
261
 
234
- ```typescript
262
+ ```tsx
235
263
  import { useSignalEffect } from "@xmachines/play-react";
236
- import { AbstractActor } from "@xmachines/play-actor";
237
264
 
238
- function CustomRenderer({ actor }: { actor: AbstractActor<any> }) {
239
- const [view, setView] = useState(null);
265
+ function NavBar({ actor }: { actor: ReturnType<typeof createPlayer> }) {
266
+ const [isAuth, setIsAuth] = useState(false);
240
267
 
241
- // Subscribe to currentView signal
242
268
  useSignalEffect(() => {
243
- const currentView = actor.currentView.get();
244
- setView(currentView);
269
+ const snap = actor.state.get();
270
+ setIsAuth((snap.context as { isAuthenticated: boolean }).isAuthenticated);
245
271
  });
246
272
 
247
- if (!view) return <div>No view</div>;
248
-
249
- // Custom rendering logic
250
- if (view.component === "SpecialCase") {
251
- return <SpecialCaseComponent {...view.props} actor={actor} />;
252
- }
253
-
254
- // Fallback to standard rendering
255
- return <DefaultComponent view={view} actor={actor} />;
273
+ return <nav>{isAuth ? <LogoutBtn /> : <LoginBtn />}</nav>;
256
274
  }
257
275
  ```
258
276
 
259
- ### Provider Pattern
277
+ ---
260
278
 
261
- ```typescript
262
- import { PlayTanStackRouterProvider } from "@xmachines/play-tanstack-react-router";
263
- import { PlayRenderer } from "@xmachines/play-react";
279
+ ### `PlayErrorBoundary`
264
280
 
265
- // Renderer receives actor via props (not children)
266
- function App() {
267
- return (
268
- <PlayTanStackRouterProvider
269
- actor={actor}
270
- router={router}
271
- renderer={(currentActor, currentRouter) => {
272
- void currentRouter;
273
- return (
274
- <div>
275
- <Header actor={currentActor} />
276
- <PlayRenderer actor={currentActor} components={components} />
277
- <Footer />
278
- </div>
279
- );
280
- }}
281
- />
282
- );
283
- }
281
+ Class error boundary that wraps the rendered output. Catches errors thrown during component render and logs them without crashing the full page. `PlayRenderer` wraps its own output in this boundary automatically.
284
282
 
285
- // Header component also receives actor
286
- function Header({ actor }: { actor: AbstractActor<any> }) {
287
- const [route, setRoute] = useState<string | null>(null);
283
+ `componentDidCatch` invokes the `onError` prop (for observability tools) but does **not** re-throw — re-throwing from `componentDidCatch` can unmount the entire React 19 root. `getDerivedStateFromError` handles fallback state transition instead.
288
284
 
289
- useSignalEffect(() => {
290
- setRoute(actor.currentRoute.get());
291
- });
285
+ ```tsx
286
+ import { PlayErrorBoundary } from "@xmachines/play-react";
292
287
 
293
- return (
294
- <header>
295
- <nav>Current: {route}</nav>
296
- </header>
297
- );
298
- }
288
+ <PlayErrorBoundary
289
+ fallback={<p>Something went wrong.</p>}
290
+ onError={(err, info) => Sentry.captureException(err, { extra: info })}
291
+ >
292
+ <PlayRenderer actor={actor} registry={registry} />
293
+ </PlayErrorBoundary>;
299
294
  ```
300
295
 
301
- ## Architecture
296
+ ---
302
297
 
303
- This package implements **Signal-Only Reactivity (INV-05)** and **Passive Infrastructure (INV-04)**:
298
+ ## Route Parameters in Props
304
299
 
305
- 1. **No Business Logic in React:**
306
- - No useState/useReducer for business state
307
- - No useEffect for side effects
308
- - React only triggers renders, doesn't control state
300
+ When using `formatPlayRouteTransitions`, URL path parameters flow automatically into component props. Declare an `undefined` slot in the spec to opt in:
309
301
 
310
- 2. **Signals as Source of Truth:**
311
- - `actor.currentView.get()` provides UI structure
312
- - `actor.currentRoute.get()` provides navigation state
313
- - Components observe signals via `useSignalEffect`
314
-
315
- 3. **Event Forwarding:**
316
- - Components receive `send` function via props
317
- - User actions send events to actor (e.g., `{ type: "auth.login" }`)
318
- - Actor guards validate and process events
319
-
320
- 4. **Microtask Batching:**
321
- - `Signal.subtle.Watcher` coalesces rapid signal changes
322
- - Prevents React thrashing from multiple signal updates
323
- - Single React render per microtask batch
324
-
325
- 5. **Explicit Disposal Contract:**
326
- - Component teardown must call watcher `unwatch` in cleanup
327
- - Do not rely on GC-only cleanup
328
-
329
- **Pattern:**
330
-
331
- - Renderer receives actor via props (provider pattern)
332
- - Enables composition with navigation, headers, footers
333
- - Supports multiple renderers in same app
334
-
335
- **Architectural Invariants:**
302
+ ```ts
303
+ // spec: { section: undefined, user: "alice" }
304
+ // After play.route to /settings/profile → context.params = { section: "profile" }
305
+ // Component receives: { section: "profile", user: "alice" }
306
+ ```
336
307
 
337
- - **Signal-Only Reactivity (INV-05):** No React state for business logic
338
- - **Passive Infrastructure (INV-04):** Components reflect, never decide
308
+ Priority: **route param fills `undefined` slots; explicit non-`undefined` spec props always win.**
339
309
 
340
- ## Benefits
310
+ ---
341
311
 
342
- - **Framework Swappable:** Business logic has zero React imports
343
- - **Type Safety:** Props validated against catalog schemas
344
- - **Simple Testing:** Test actors without React renderer
345
- - **Performance:** Microtask batching reduces unnecessary renders
346
- - **Composability:** Renderer prop enables complex layouts ()
312
+ ## Error Handling
347
313
 
348
- ## Related Packages
314
+ | Error | Cause | Fix |
315
+ | ------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------- |
316
+ | `useActor() must be called inside <PlayRenderer>` | Hook called outside the tree | Move inside a component rendered by `PlayRenderer` |
317
+ | Component render error | Component throws | `PlayErrorBoundary` catches it; inspect component implementation |
349
318
 
350
- - **[@xmachines/play-xstate](../play-xstate)** - XState adapter providing actors
351
- - **[@xmachines/play-catalog](../play-catalog)** - UI schema validation
352
- - **[@xmachines/play-tanstack-react-router](../play-tanstack-react-router)** - TanStack Router integration
353
- - **[@xmachines/play-actor](../play-actor)** - Actor base
354
- - **[@xmachines/play-signals](../play-signals)** - TC39 Signals primitives
319
+ ---
355
320
 
356
- ## License
321
+ ## Architecture Notes
357
322
 
358
- MIT
323
+ - React `useState` is **only** used to trigger re-renders — never for business logic
324
+ - `actor.currentView` (TC39 Signal) is the sole render trigger; `PlayRenderer` is a passive observer
325
+ - Per-view UI state lives in an `@xstate/store` atom, not in React state
326
+ - `@json-render/react` drives rendering; `PlayRenderer` is the signal bridge — import `defineRegistry`, `ComponentFn`, `ComponentContext`, and `useStateBinding` from `@xmachines/play-react`