@xmachines/play-react 1.0.0-beta.46 → 1.0.0-beta.48
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/README.md +109 -320
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -1,172 +1,54 @@
|
|
|
1
|
+
<!-- generated-by: gsd-doc-writer -->
|
|
2
|
+
|
|
1
3
|
# @xmachines/play-react
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
React renderer for XMachines Play architecture with signal-driven rendering.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Part of the [xmachines-js monorepo](../../README.md).
|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## Installation
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npm install @xmachines/play-react
|
|
13
|
+
```
|
|
10
14
|
|
|
11
|
-
|
|
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)
|
|
15
|
+
**Peer dependencies** (must be installed separately):
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
```bash
|
|
18
|
+
npm install react react-dom xstate @xstate/store @json-render/react @json-render/core @json-render/xstate
|
|
19
|
+
```
|
|
17
20
|
|
|
18
|
-
|
|
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
|
|
21
|
+
Supported versions:
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
- `react` / `react-dom`: `^18.0.0 || ^19.0.0`
|
|
24
|
+
- `xstate`: `^5.30.0`
|
|
25
|
+
- `@xstate/store`: `^3.17.0`
|
|
26
|
+
- `@json-render/*`: `^0.18.0`
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
npm install @json-render/xstate @xstate/store # store integration
|
|
28
|
-
```
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Standard usage — `PlayUIProvider` + `PlayRenderer`
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
so `defineRegistry(..., { onRenderError })` can intercept inner element-boundary errors
|
|
32
|
-
without muting console output.
|
|
33
|
-
|
|
34
|
-
## Current Exports
|
|
35
|
-
|
|
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
|
-
- `useBoundProp` — re-exported from `@json-render/react`
|
|
42
|
-
- `ComponentFn` (type) — re-exported from `@json-render/react`
|
|
43
|
-
- `ComponentContext` (type) — re-exported from `@json-render/react`
|
|
44
|
-
- `ActorProvider` — escape hatch primitive (owns actor bridging, signal bridge, store lifecycle)
|
|
45
|
-
- `PlayUIProvider` — batteries-included composite (wraps `ActorProvider` + `JSONUIProvider`)
|
|
46
|
-
- `usePlayView` — hook for accessing the current view spec inside a provider tree
|
|
47
|
-
- `RenderErrorHandler` (type) — inner per-element error callback signature
|
|
48
|
-
- `ActorProviderProps` (type)
|
|
49
|
-
- `ViewContextValue` (type)
|
|
50
|
-
- `PlayActor` (type)
|
|
51
|
-
|
|
52
|
-
## Quick Start
|
|
32
|
+
The recommended pattern for actor-driven React rendering:
|
|
53
33
|
|
|
54
34
|
```tsx
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
import { z } from "zod";
|
|
62
|
-
|
|
63
|
-
// 1. Define catalog — the contract between machine spec and UI components
|
|
64
|
-
const catalog = defineCatalog({
|
|
65
|
-
elements: {
|
|
66
|
-
Login: { props: z.object({ title: z.string() }), description: "Login form" },
|
|
67
|
-
Dashboard: { props: z.object({ username: z.string() }), description: "Dashboard" },
|
|
68
|
-
},
|
|
69
|
-
});
|
|
35
|
+
import { PlayUIProvider, PlayRenderer, defineRegistry } from "@xmachines/play-react";
|
|
36
|
+
import { definePlayer } from "@xmachines/play-xstate";
|
|
37
|
+
|
|
38
|
+
// 1. Create and start the actor
|
|
39
|
+
const actor = definePlayer({ machine: myMachine })();
|
|
40
|
+
actor.start();
|
|
70
41
|
|
|
71
|
-
// 2.
|
|
72
|
-
const
|
|
73
|
-
<form
|
|
74
|
-
onSubmit={(e) => {
|
|
75
|
-
e.preventDefault();
|
|
76
|
-
emit("submit");
|
|
77
|
-
}}
|
|
78
|
-
>
|
|
79
|
-
<h2>{props.title}</h2>
|
|
80
|
-
<button type="submit">Log In</button>
|
|
81
|
-
</form>
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const Dashboard: ComponentFn<typeof catalog, "Dashboard"> = ({ props }) => (
|
|
85
|
-
<div>Welcome, {props.username}!</div>
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// 3. Build registry — wires components to catalog and declares action handlers
|
|
89
|
-
const registryResult = defineRegistry(catalog, {
|
|
42
|
+
// 2. Define the component registry with action handlers
|
|
43
|
+
const registryResult = defineRegistry(myCatalog, {
|
|
90
44
|
components: { Login, Dashboard },
|
|
91
45
|
actions: {
|
|
92
|
-
login: async (
|
|
93
|
-
|
|
94
|
-
actor.send({ type: "auth.login", username: params.username });
|
|
95
|
-
},
|
|
96
|
-
logout: async () => {
|
|
97
|
-
actor.send({ type: "auth.logout" });
|
|
98
|
-
},
|
|
46
|
+
login: async ({ username }) => actor.send({ type: "auth.login", username }),
|
|
47
|
+
logout: async () => actor.send({ type: "auth.logout" }),
|
|
99
48
|
},
|
|
100
49
|
});
|
|
101
50
|
|
|
102
|
-
//
|
|
103
|
-
const machine = setup({
|
|
104
|
-
types: {
|
|
105
|
-
context: {} as {
|
|
106
|
-
isAuthenticated: boolean;
|
|
107
|
-
username: string | null;
|
|
108
|
-
params: Record<string, string>;
|
|
109
|
-
query: Record<string, string>;
|
|
110
|
-
},
|
|
111
|
-
events: {} as
|
|
112
|
-
| { type: "auth.login"; username: string }
|
|
113
|
-
| { type: "auth.logout" }
|
|
114
|
-
| { type: "play.route"; to: string; params?: Record<string, string> },
|
|
115
|
-
},
|
|
116
|
-
}).createMachine(
|
|
117
|
-
formatPlayRouteTransitions({
|
|
118
|
-
id: "app",
|
|
119
|
-
initial: "login",
|
|
120
|
-
context: { isAuthenticated: false, username: null, params: {}, query: {} },
|
|
121
|
-
states: {
|
|
122
|
-
login: {
|
|
123
|
-
id: "login",
|
|
124
|
-
meta: {
|
|
125
|
-
route: "/login",
|
|
126
|
-
view: {
|
|
127
|
-
root: "root",
|
|
128
|
-
elements: {
|
|
129
|
-
root: { type: "Login", props: { title: "Sign In" }, children: [] },
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
dashboard: {
|
|
135
|
-
id: "dashboard",
|
|
136
|
-
meta: {
|
|
137
|
-
route: "/dashboard",
|
|
138
|
-
view: {
|
|
139
|
-
root: "root",
|
|
140
|
-
elements: {
|
|
141
|
-
root: { type: "Dashboard", props: { username: "" }, children: [] },
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
on: {
|
|
148
|
-
"auth.login": {
|
|
149
|
-
target: ".dashboard",
|
|
150
|
-
guard: ({ context }) => !context.isAuthenticated,
|
|
151
|
-
actions: assign({
|
|
152
|
-
isAuthenticated: true,
|
|
153
|
-
username: ({ event }) => event.username,
|
|
154
|
-
}),
|
|
155
|
-
},
|
|
156
|
-
"auth.logout": {
|
|
157
|
-
target: ".login",
|
|
158
|
-
guard: ({ context }) => context.isAuthenticated,
|
|
159
|
-
actions: assign({ isAuthenticated: false, username: null }),
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
}),
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
// 5. Create actor and render
|
|
166
|
-
const createPlayer = definePlayer({ machine });
|
|
167
|
-
const actor = createPlayer();
|
|
168
|
-
actor.start();
|
|
169
|
-
|
|
51
|
+
// 3. Render — signals drive view transitions automatically
|
|
170
52
|
function App() {
|
|
171
53
|
return (
|
|
172
54
|
<PlayUIProvider actor={actor} registryResult={registryResult}>
|
|
@@ -176,229 +58,136 @@ function App() {
|
|
|
176
58
|
}
|
|
177
59
|
```
|
|
178
60
|
|
|
179
|
-
|
|
61
|
+
### With optional `JSONUIProvider` props
|
|
180
62
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
Batteries-included composite provider. Wraps `ActorProvider` + `JSONUIProvider`. Pass `actor` and `registryResult` here, then place `<PlayRenderer />` inside as a zero-prop child.
|
|
63
|
+
Pass navigation and validation helpers through `PlayUIProvider`:
|
|
184
64
|
|
|
185
65
|
```tsx
|
|
186
66
|
<PlayUIProvider
|
|
187
|
-
actor={actor} // required
|
|
188
|
-
registryResult={registryResult} // required
|
|
189
|
-
store={myStore} // optional — controlled mode
|
|
190
|
-
fallback={<p>Loading…</p>} // optional
|
|
191
|
-
onError={(err, info) => Sentry.captureException(err, { extra: info })} // optional
|
|
192
|
-
onRenderError={(error, elementType) => console.warn(`<${elementType}> crashed:`, error)} // optional
|
|
193
|
-
>
|
|
194
|
-
<PlayRenderer />
|
|
195
|
-
</PlayUIProvider>
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
**`actor`** — A `PlayerActor` (or any `AbstractActor & Viewable`). Provides the `currentView` signal.
|
|
199
|
-
|
|
200
|
-
**`registryResult`** — Full result from `defineRegistry(catalog, { components, actions })`. Contains both the component registry and action handlers.
|
|
201
|
-
|
|
202
|
-
**`store`** (optional) — Controls per-view UI state (form values, `$state` bindings):
|
|
203
|
-
|
|
204
|
-
- **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.
|
|
205
|
-
- **Provided (controlled):** The caller owns the store lifecycle. `spec.state` is ignored.
|
|
206
|
-
|
|
207
|
-
```tsx
|
|
208
|
-
import { createAtom } from "@xstate/store";
|
|
209
|
-
import { xstateStoreStateStore } from "@json-render/xstate";
|
|
210
|
-
import type { StateStore } from "@json-render/core";
|
|
211
|
-
|
|
212
|
-
const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "alice" }) });
|
|
213
|
-
|
|
214
|
-
<PlayUIProvider actor={actor} registryResult={registryResult} store={store}>
|
|
215
|
-
<PlayRenderer />
|
|
216
|
-
</PlayUIProvider>;
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
**`fallback`** — Shown when `actor.currentView.get()` is `null` (machine in a no-view state, or during initialisation).
|
|
220
|
-
|
|
221
|
-
**`onError`** — Called when the outer `PlayErrorBoundary` catches an error. Receives `(error: Error, info: React.ErrorInfo)`. Use for observability tools (Sentry, Datadog, etc.).
|
|
222
|
-
|
|
223
|
-
**`onRenderError`** — Called when an individual catalog component throws during render. Caught by `@json-render/react`'s inner per-element boundary — the failed component is silently removed while the rest of the spec continues rendering. `onError` / `fallback` are **not** triggered. When both `onRenderError` on `PlayUIProvider` and on `defineRegistry` are set, the prop wins.
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
### `ActorProvider`
|
|
228
|
-
|
|
229
|
-
Escape hatch primitive. Owns actor bridging, signal bridge, and store lifecycle. Use this when you need direct control over the provider layer (e.g. custom `JSONUIProvider` configuration).
|
|
230
|
-
|
|
231
|
-
```tsx
|
|
232
|
-
import { ActorProvider } from "@xmachines/play-react";
|
|
233
|
-
|
|
234
|
-
<ActorProvider
|
|
235
67
|
actor={actor}
|
|
236
68
|
registryResult={registryResult}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
{/* your own JSONUIProvider + PlayRenderer tree */}
|
|
240
|
-
</ActorProvider>;
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
### `PlayRenderer`
|
|
246
|
-
|
|
247
|
-
Zero-prop leaf component. Must be rendered inside a `PlayUIProvider` (or `ActorProvider`) tree. Subscribes to `actor.currentView` via context and renders the current spec.
|
|
248
|
-
|
|
249
|
-
```tsx
|
|
250
|
-
<PlayUIProvider actor={actor} registryResult={registryResult}>
|
|
251
|
-
<PlayRenderer />
|
|
252
|
-
</PlayUIProvider>
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
`PlayRenderer` accepts no props — all configuration (`actor`, `registryResult`, `store`, `fallback`, `onError`, `onRenderError`) is provided by the enclosing `PlayUIProvider` or `ActorProvider`.
|
|
256
|
-
|
|
257
|
-
## Error handling
|
|
258
|
-
|
|
259
|
-
The provider tree has two layers of error boundaries:
|
|
260
|
-
|
|
261
|
-
### Outer boundary — `onError` and `fallback`
|
|
262
|
-
|
|
263
|
-
Wraps the entire renderer output via `PlayErrorBoundary`. Triggered when the spec or store setup throws, or when the inner boundary is not present.
|
|
264
|
-
|
|
265
|
-
```tsx
|
|
266
|
-
<PlayUIProvider
|
|
267
|
-
actor={actor}
|
|
268
|
-
registryResult={registryResult}
|
|
269
|
-
fallback={<p>Something went wrong.</p>}
|
|
270
|
-
onError={(err, info) => Sentry.captureException(err, { extra: info })}
|
|
69
|
+
navigate={(path) => router.push(path)}
|
|
70
|
+
validationFunctions={{ isEmail: (v) => /^.+@.+$/.test(String(v)) }}
|
|
271
71
|
>
|
|
272
72
|
<PlayRenderer />
|
|
273
73
|
</PlayUIProvider>
|
|
274
74
|
```
|
|
275
75
|
|
|
276
|
-
###
|
|
76
|
+
### Escape hatch — custom provider composition
|
|
277
77
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
Pass `onRenderError` to `PlayUIProvider` (or `ActorProvider`) — overrides any registry-level handler — or bake it into `defineRegistry`:
|
|
78
|
+
Use `ActorProvider` directly when you need to compose providers manually:
|
|
281
79
|
|
|
282
80
|
```tsx
|
|
283
|
-
|
|
284
|
-
<PlayUIProvider
|
|
285
|
-
actor={actor}
|
|
286
|
-
registryResult={registryResult}
|
|
287
|
-
onRenderError={(error, elementType) => {
|
|
288
|
-
console.warn(`<${elementType}> crashed:`, error);
|
|
289
|
-
}}
|
|
290
|
-
>
|
|
291
|
-
<PlayRenderer />
|
|
292
|
-
</PlayUIProvider>
|
|
293
|
-
```
|
|
81
|
+
import { ActorProvider, JSONUIProvider, PlayRenderer } from "@xmachines/play-react";
|
|
294
82
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
onRenderError(error, elementType) {
|
|
301
|
-
reportExpectedRenderError(error, elementType);
|
|
302
|
-
},
|
|
303
|
-
});
|
|
83
|
+
<ActorProvider actor={actor} registryResult={registryResult}>
|
|
84
|
+
<JSONUIProvider registry={registryResult.registry}>
|
|
85
|
+
<PlayRenderer />
|
|
86
|
+
</JSONUIProvider>
|
|
87
|
+
</ActorProvider>;
|
|
304
88
|
```
|
|
305
89
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
---
|
|
309
|
-
|
|
310
|
-
### `useActor`
|
|
311
|
-
|
|
312
|
-
Hook for accessing the actor from inside any component rendered by `PlayRenderer`. No prop drilling needed.
|
|
90
|
+
### Accessing the actor from inside the tree
|
|
313
91
|
|
|
314
92
|
```tsx
|
|
315
93
|
import { useActor } from "@xmachines/play-react";
|
|
316
94
|
|
|
317
|
-
|
|
318
|
-
function LogoutButton() {
|
|
95
|
+
function SubmitButton() {
|
|
319
96
|
const actor = useActor();
|
|
320
|
-
return <button onClick={() => actor.send({ type: "
|
|
97
|
+
return <button onClick={() => actor.send({ type: "SUBMIT" })}>Submit</button>;
|
|
321
98
|
}
|
|
322
99
|
```
|
|
323
100
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
|
-
### `useSignalEffect`
|
|
329
|
-
|
|
330
|
-
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).
|
|
101
|
+
### Subscribing to signals directly
|
|
331
102
|
|
|
332
103
|
```tsx
|
|
333
104
|
import { useSignalEffect } from "@xmachines/play-react";
|
|
334
105
|
|
|
335
|
-
function
|
|
336
|
-
const [
|
|
106
|
+
function MyComponent({ actor }) {
|
|
107
|
+
const [view, setView] = useState(null);
|
|
337
108
|
|
|
338
109
|
useSignalEffect(() => {
|
|
339
|
-
|
|
340
|
-
setIsAuth((snap.context as { isAuthenticated: boolean }).isAuthenticated);
|
|
110
|
+
setView(actor.currentView.get());
|
|
341
111
|
});
|
|
342
112
|
|
|
343
|
-
return <
|
|
113
|
+
return <div>{view?.component}</div>;
|
|
344
114
|
}
|
|
345
115
|
```
|
|
346
116
|
|
|
347
|
-
|
|
117
|
+
## API Summary
|
|
348
118
|
|
|
349
|
-
###
|
|
119
|
+
### Components
|
|
350
120
|
|
|
351
|
-
|
|
121
|
+
| Export | Description |
|
|
122
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `<PlayUIProvider>` | Batteries-included provider: wraps `ActorProvider` + `JSONUIProvider`. Standard entry point. |
|
|
124
|
+
| `<PlayRenderer>` | Zero-prop leaf component. Reads the current actor view from context and renders it. Must be inside `PlayUIProvider` or `ActorProvider`. |
|
|
125
|
+
| `<ActorProvider>` | Escape-hatch primitive. Owns actor bridging, signal subscription, and per-view `StateStore` lifecycle. |
|
|
126
|
+
| `<PlayErrorBoundary>` | React class error boundary for catching catalog component render errors. |
|
|
352
127
|
|
|
353
|
-
|
|
128
|
+
### Hooks
|
|
354
129
|
|
|
355
|
-
|
|
356
|
-
|
|
130
|
+
| Export | Description |
|
|
131
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
132
|
+
| `useSignalEffect(callback)` | Subscribes to TC39 signal changes; re-runs the callback and forces a React re-render when any accessed signal changes. Cleanup is automatic on unmount. |
|
|
133
|
+
| `useActor()` | Returns the raw actor instance. Must be called inside an `ActorProvider`/`PlayUIProvider` tree. |
|
|
134
|
+
| `usePlayView()` | Returns `{ spec, handlers, registry, store }` for the current view. Must be called inside an `ActorProvider`/`PlayUIProvider` tree. |
|
|
357
135
|
|
|
358
|
-
|
|
359
|
-
fallback={<p>Something went wrong.</p>}
|
|
360
|
-
onError={(err, info) => Sentry.captureException(err, { extra: info })}
|
|
361
|
-
>
|
|
362
|
-
<PlayUIProvider actor={actor} registryResult={registryResult}>
|
|
363
|
-
<PlayRenderer />
|
|
364
|
-
</PlayUIProvider>
|
|
365
|
-
</PlayErrorBoundary>;
|
|
366
|
-
```
|
|
136
|
+
### Types
|
|
367
137
|
|
|
368
|
-
|
|
138
|
+
| Export | Description |
|
|
139
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
140
|
+
| `PlayUIProviderProps` | Props for `<PlayUIProvider>` |
|
|
141
|
+
| `ActorProviderProps` | Props for `<ActorProvider>` (also exported as `PlayRendererProps` for migration compatibility) |
|
|
142
|
+
| `PlayErrorBoundaryProps` | Props for `<PlayErrorBoundary>` |
|
|
143
|
+
| `PlayErrorBoundaryState` | State shape for `<PlayErrorBoundary>` |
|
|
144
|
+
| `PlayActor` | Type alias for `AbstractActor<AnyActorLogic>` |
|
|
145
|
+
| `ViewContextValue` | Value shape returned by `usePlayView()` |
|
|
146
|
+
| `RenderErrorHandler` | Error handler callback type for render errors |
|
|
369
147
|
|
|
370
|
-
|
|
148
|
+
### Re-exports from `@json-render/react`
|
|
371
149
|
|
|
372
|
-
|
|
150
|
+
`@xmachines/play-react` re-exports the full `@json-render/react` surface so consumers only need one import:
|
|
373
151
|
|
|
374
152
|
```ts
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
153
|
+
import {
|
|
154
|
+
defineRegistry,
|
|
155
|
+
useBoundProp,
|
|
156
|
+
JSONUIProvider,
|
|
157
|
+
StateProvider,
|
|
158
|
+
ActionProvider,
|
|
159
|
+
VisibilityProvider,
|
|
160
|
+
ValidationProvider,
|
|
161
|
+
Renderer,
|
|
162
|
+
} from "@xmachines/play-react";
|
|
378
163
|
```
|
|
379
164
|
|
|
380
|
-
|
|
165
|
+
## Key Principle
|
|
166
|
+
|
|
167
|
+
React state is **never** used for business logic — only for triggering React's render cycle. Signals (`@xmachines/play-signals`) are the source of truth. `PlayUIProvider` passively observes actor signals via `useSignalEffect` and re-renders when the current view changes. Rapid signal updates are batched via microtasks to prevent unnecessary React renders.
|
|
168
|
+
|
|
169
|
+
## Testing
|
|
170
|
+
|
|
171
|
+
Run unit tests (jsdom environment):
|
|
381
172
|
|
|
382
|
-
|
|
173
|
+
```bash
|
|
174
|
+
npm test -w @xmachines/play-react
|
|
175
|
+
```
|
|
383
176
|
|
|
384
|
-
|
|
177
|
+
Run tests with coverage:
|
|
385
178
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
| Component render error | Component throws | `PlayErrorBoundary` catches it; inspect component implementation |
|
|
179
|
+
```bash
|
|
180
|
+
npm run test:coverage -w @xmachines/play-react
|
|
181
|
+
```
|
|
390
182
|
|
|
391
|
-
|
|
183
|
+
Run browser integration tests (requires Chromium):
|
|
392
184
|
|
|
393
|
-
|
|
185
|
+
```bash
|
|
186
|
+
npm run test:browser -w @xmachines/play-react
|
|
187
|
+
```
|
|
394
188
|
|
|
395
|
-
|
|
396
|
-
- `actor.currentView` (TC39 Signal) is the sole render trigger; `PlayRenderer` is a passive observer
|
|
397
|
-
- Per-view UI state lives in an `@xstate/store` atom, not in React state
|
|
398
|
-
- `@json-render/react` drives rendering; `PlayRenderer` is the signal bridge — import `defineRegistry`, `ComponentFn`, `ComponentContext`, and `useBoundProp` from
|
|
189
|
+
Coverage thresholds: 80% lines, functions, branches, and statements.
|
|
399
190
|
|
|
400
|
-
##
|
|
191
|
+
## License
|
|
401
192
|
|
|
402
|
-
|
|
403
|
-
- [React Router adapter](../play-react-router/README.md)
|
|
404
|
-
- [TanStack React Router adapter](../play-tanstack-react-router/README.md)
|
|
193
|
+
MIT — see [LICENSE](./LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmachines/play-react",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.48",
|
|
4
4
|
"description": "React renderer for XMachines Play architecture with signal-driven rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"actor",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"prepublishOnly": "npm run build"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@xmachines/play": "1.0.0-beta.
|
|
49
|
-
"@xmachines/play-actor": "1.0.0-beta.
|
|
50
|
-
"@xmachines/play-signals": "1.0.0-beta.
|
|
48
|
+
"@xmachines/play": "1.0.0-beta.48",
|
|
49
|
+
"@xmachines/play-actor": "1.0.0-beta.48",
|
|
50
|
+
"@xmachines/play-signals": "1.0.0-beta.48"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@json-render/core": "^0.18.0",
|
|
@@ -58,16 +58,16 @@
|
|
|
58
58
|
"@types/node": "^25.6.0",
|
|
59
59
|
"@types/react": "^19.2.14",
|
|
60
60
|
"@types/react-dom": "^19.2.3",
|
|
61
|
-
"@xmachines/shared": "1.0.0-beta.
|
|
61
|
+
"@xmachines/shared": "1.0.0-beta.48",
|
|
62
62
|
"@xstate/store": "^3.17.0",
|
|
63
|
-
"jsdom": "^29.0
|
|
64
|
-
"oxfmt": "^0.
|
|
65
|
-
"oxlint": "^1.
|
|
63
|
+
"jsdom": "^29.1.0",
|
|
64
|
+
"oxfmt": "^0.47.0",
|
|
65
|
+
"oxlint": "^1.62.0",
|
|
66
66
|
"react": "^19.2.5",
|
|
67
67
|
"react-dom": "^19.2.5",
|
|
68
68
|
"typescript": "^5.9.3 || ^6.0.3",
|
|
69
|
-
"vitest": "^4.1.
|
|
70
|
-
"xstate": "^5.
|
|
69
|
+
"vitest": "^4.1.5",
|
|
70
|
+
"xstate": "^5.31.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"@json-render/core": "^0.18.0",
|
|
@@ -76,6 +76,6 @@
|
|
|
76
76
|
"@xstate/store": "^3.17.0",
|
|
77
77
|
"react": "^18.0.0 || ^19.0.0",
|
|
78
78
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
79
|
-
"xstate": "^5.
|
|
79
|
+
"xstate": "^5.31.0"
|
|
80
80
|
}
|
|
81
81
|
}
|