@xmachines/play-react 1.0.0-beta.9 → 1.0.0
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 +21 -0
- package/README.md +130 -332
- package/dist/ActorProvider.d.ts +82 -0
- package/dist/ActorProvider.d.ts.map +1 -0
- package/dist/ActorProvider.js +178 -0
- package/dist/ActorProvider.js.map +1 -0
- package/dist/PlayErrorBoundary.d.ts +10 -2
- package/dist/PlayErrorBoundary.d.ts.map +1 -1
- package/dist/PlayErrorBoundary.js +4 -1
- package/dist/PlayErrorBoundary.js.map +1 -1
- package/dist/PlayRenderer.d.ts +26 -47
- package/dist/PlayRenderer.d.ts.map +1 -1
- package/dist/PlayRenderer.js +31 -77
- package/dist/PlayRenderer.js.map +1 -1
- package/dist/PlayUIProvider.d.ts +67 -0
- package/dist/PlayUIProvider.d.ts.map +1 -0
- package/dist/PlayUIProvider.js +73 -0
- package/dist/PlayUIProvider.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +30 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +6 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/dist/useActor.d.ts +27 -0
- package/dist/useActor.d.ts.map +1 -0
- package/dist/useActor.js +27 -0
- package/dist/useActor.js.map +1 -0
- package/dist/useSignalEffect.d.ts +30 -15
- package/dist/useSignalEffect.d.ts.map +1 -1
- package/dist/useSignalEffect.js +43 -43
- package/dist/useSignalEffect.js.map +1 -1
- package/package.json +35 -14
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,395 +1,193 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
**React renderer consuming signals and UI schema with provider pattern**
|
|
4
|
-
|
|
5
|
-
Signal-driven React rendering layer observing actor state with zero React state for business logic.
|
|
1
|
+
<!-- generated-by: gsd-doc-writer -->
|
|
6
2
|
|
|
7
|
-
|
|
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.
|
|
3
|
+
# @xmachines/play-react
|
|
10
4
|
|
|
11
|
-
|
|
5
|
+
React renderer for XMachines Play architecture with signal-driven rendering.
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
- **Passive Infrastructure (INV-04):** Components observe signals, send events to actor
|
|
7
|
+
Part of the [xmachines-js monorepo](../../README.md).
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
## Installation
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @xmachines/play-react
|
|
13
|
+
```
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
**Peer dependencies** (must be installed separately):
|
|
21
16
|
|
|
22
17
|
```bash
|
|
23
|
-
|
|
24
|
-
npm install @xmachines/play-react
|
|
18
|
+
pnpm add react react-dom xstate @xstate/store @xmachines/json-render-react @xmachines/json-render-core @xmachines/json-render-xstate
|
|
25
19
|
```
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
Supported versions:
|
|
28
22
|
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
32
|
-
- `
|
|
33
|
-
- `PlayErrorBoundaryProps` (type)
|
|
23
|
+
- `react` / `react-dom`: `^18.0.0 || ^19.0.0`
|
|
24
|
+
- `xstate`: `^5.31.0`
|
|
25
|
+
- `@xstate/store`: `^3.17.0`
|
|
26
|
+
- `@xmachines/json-render-*`: `^0.18.0`
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
## Usage
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
- `react-dom` ^18.0.0 || ^19.0.0 — React DOM renderer
|
|
30
|
+
### Standard usage — `PlayUIProvider` + `PlayRenderer`
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
The recommended pattern for actor-driven React rendering:
|
|
41
33
|
|
|
42
|
-
```
|
|
43
|
-
import {
|
|
34
|
+
```tsx
|
|
35
|
+
import { PlayUIProvider, PlayRenderer, defineRegistry } from "@xmachines/play-react";
|
|
44
36
|
import { definePlayer } from "@xmachines/play-xstate";
|
|
45
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
46
|
-
import { PlayRenderer } from "@xmachines/play-react";
|
|
47
|
-
import { z } from "zod";
|
|
48
|
-
|
|
49
|
-
// 1. Define catalog (business logic layer)
|
|
50
|
-
const catalog = defineCatalog({
|
|
51
|
-
LoginForm: z.object({ error: z.string().optional() }),
|
|
52
|
-
Dashboard: z.object({
|
|
53
|
-
userId: z.string(),
|
|
54
|
-
username: z.string(),
|
|
55
|
-
}),
|
|
56
|
-
});
|
|
57
37
|
|
|
58
|
-
//
|
|
59
|
-
const
|
|
60
|
-
LoginForm: ({ error, send }) => (
|
|
61
|
-
<form
|
|
62
|
-
onSubmit={(e) => {
|
|
63
|
-
e.preventDefault();
|
|
64
|
-
const data = new FormData(e.currentTarget);
|
|
65
|
-
send({
|
|
66
|
-
type: "auth.login",
|
|
67
|
-
username: data.get("username"),
|
|
68
|
-
});
|
|
69
|
-
}}
|
|
70
|
-
>
|
|
71
|
-
{error && <p style={{ color: "red" }}>{error}</p>}
|
|
72
|
-
<input name="username" required placeholder="Username" />
|
|
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 });
|
|
87
|
-
const actor = createPlayer();
|
|
38
|
+
// 1. Create and start the actor
|
|
39
|
+
const actor = definePlayer({ machine: myMachine })();
|
|
88
40
|
actor.start();
|
|
89
41
|
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Main renderer component subscribing to actor signals and dynamically rendering catalog components:
|
|
42
|
+
// 2. Define the component registry with action handlers
|
|
43
|
+
const registryResult = defineRegistry(myCatalog, {
|
|
44
|
+
components: { Login, Dashboard },
|
|
45
|
+
actions: {
|
|
46
|
+
login: async ({ username }) => actor.send({ type: "auth.login", username }),
|
|
47
|
+
logout: async () => actor.send({ type: "auth.logout" }),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
100
50
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
51
|
+
// 3. Render — signals drive view transitions automatically
|
|
52
|
+
function App() {
|
|
53
|
+
return (
|
|
54
|
+
<PlayUIProvider actor={actor} registryResult={registryResult}>
|
|
55
|
+
<PlayRenderer />
|
|
56
|
+
</PlayUIProvider>
|
|
57
|
+
);
|
|
106
58
|
}
|
|
107
59
|
```
|
|
108
60
|
|
|
109
|
-
|
|
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`)
|
|
114
|
-
|
|
115
|
-
**Behavior:**
|
|
61
|
+
### With optional `JSONUIProvider` props
|
|
116
62
|
|
|
117
|
-
|
|
118
|
-
2. Looks up component from `components` map using `view.component` string
|
|
119
|
-
3. Renders component with props from `view.props` + `send` function
|
|
63
|
+
Pass navigation and validation helpers through `PlayUIProvider`:
|
|
120
64
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
<PlayRenderer
|
|
65
|
+
```tsx
|
|
66
|
+
<PlayUIProvider
|
|
125
67
|
actor={actor}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### useSignalEffect()
|
|
135
|
-
|
|
136
|
-
Hook for subscribing to signal changes with automatic cleanup:
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
useSignalEffect(() => {
|
|
140
|
-
const value = signal.get();
|
|
141
|
-
// React re-renders when signal changes
|
|
142
|
-
});
|
|
68
|
+
registryResult={registryResult}
|
|
69
|
+
navigate={(path) => router.push(path)}
|
|
70
|
+
validationFunctions={{ isEmail: (v) => /^.+@.+$/.test(String(v)) }}
|
|
71
|
+
>
|
|
72
|
+
<PlayRenderer />
|
|
73
|
+
</PlayUIProvider>
|
|
143
74
|
```
|
|
144
75
|
|
|
145
|
-
|
|
146
|
-
|
|
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`
|
|
151
|
-
|
|
152
|
-
**Canonical watcher lifecycle:**
|
|
153
|
-
|
|
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()`
|
|
76
|
+
### Escape hatch — custom provider composition
|
|
159
77
|
|
|
160
|
-
|
|
78
|
+
Use `ActorProvider` directly when you need to compose providers manually:
|
|
161
79
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
import { useSignalEffect } from "@xmachines/play-react";
|
|
166
|
-
import { useState } from "react";
|
|
167
|
-
|
|
168
|
-
function RouteDisplay({ actor }: { actor: AbstractActor<any> }) {
|
|
169
|
-
const [route, setRoute] = useState<string | null>(null);
|
|
170
|
-
|
|
171
|
-
useSignalEffect(() => {
|
|
172
|
-
const currentRoute = actor.currentRoute.get();
|
|
173
|
-
setRoute(currentRoute);
|
|
174
|
-
});
|
|
80
|
+
```tsx
|
|
81
|
+
import { ActorProvider, JSONUIProvider, PlayRenderer } from "@xmachines/play-react";
|
|
175
82
|
|
|
176
|
-
|
|
177
|
-
}
|
|
83
|
+
<ActorProvider actor={actor} registryResult={registryResult}>
|
|
84
|
+
<JSONUIProvider registry={registryResult.registry}>
|
|
85
|
+
<PlayRenderer />
|
|
86
|
+
</JSONUIProvider>
|
|
87
|
+
</ActorProvider>;
|
|
178
88
|
```
|
|
179
89
|
|
|
180
|
-
###
|
|
181
|
-
|
|
182
|
-
React class component error boundary for catching catalog component render errors.
|
|
90
|
+
### Accessing the actor from inside the tree
|
|
183
91
|
|
|
184
|
-
|
|
92
|
+
```tsx
|
|
93
|
+
import { useActor } from "@xmachines/play-react";
|
|
185
94
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
children: React.ReactNode;
|
|
190
|
-
onError?: (error: Error, info: React.ErrorInfo) => void; // Forward to Sentry, Datadog, etc.
|
|
95
|
+
function SubmitButton() {
|
|
96
|
+
const actor = useActor();
|
|
97
|
+
return <button onClick={() => actor.send({ type: "SUBMIT" })}>Submit</button>;
|
|
191
98
|
}
|
|
192
99
|
```
|
|
193
100
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
- `fallback` — ReactNode rendered when a child component throws. Defaults to `null`.
|
|
197
|
-
- `onError` — Optional callback forwarded on every caught error. Use for production observability (Sentry, Datadog, custom logging).
|
|
198
|
-
|
|
199
|
-
**Example:**
|
|
101
|
+
### Subscribing to signals directly
|
|
200
102
|
|
|
201
103
|
```tsx
|
|
202
|
-
import { PlayErrorBoundary } from "@xmachines/play-react";
|
|
203
|
-
|
|
204
|
-
<PlayErrorBoundary
|
|
205
|
-
fallback={<div className="error">Something went wrong.</div>}
|
|
206
|
-
onError={(error) => Sentry.captureException(error)}
|
|
207
|
-
>
|
|
208
|
-
<YourCatalogComponent />
|
|
209
|
-
</PlayErrorBoundary>;
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Works with React 18 and React 19. Uses the standard class component `componentDidCatch` + `getDerivedStateFromError` pattern.
|
|
213
|
-
|
|
214
|
-
**Complete API:** See [API Documentation](../../docs/api/@xmachines/play-react)
|
|
215
|
-
|
|
216
|
-
## Examples
|
|
217
|
-
|
|
218
|
-
### Component Receiving Props from Catalog
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
import { PlayRenderer } from "@xmachines/play-react";
|
|
222
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
223
|
-
import { z } from "zod";
|
|
224
|
-
|
|
225
|
-
// Define schema in catalog
|
|
226
|
-
const catalog = defineCatalog({
|
|
227
|
-
UserProfile: z.object({
|
|
228
|
-
userId: z.string(),
|
|
229
|
-
name: z.string(),
|
|
230
|
-
avatar: z.string().url().optional(),
|
|
231
|
-
stats: z.object({
|
|
232
|
-
posts: z.number(),
|
|
233
|
-
followers: z.number(),
|
|
234
|
-
}),
|
|
235
|
-
}),
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// Component receives type-safe props + send
|
|
239
|
-
const components = {
|
|
240
|
-
UserProfile: ({ userId, name, avatar, stats, send }) => (
|
|
241
|
-
<div>
|
|
242
|
-
{avatar && <img src={avatar} alt={name} />}
|
|
243
|
-
<h1>{name}</h1>
|
|
244
|
-
<p>ID: {userId}</p>
|
|
245
|
-
<div>
|
|
246
|
-
<span>{stats.posts} posts</span>
|
|
247
|
-
<span>{stats.followers} followers</span>
|
|
248
|
-
</div>
|
|
249
|
-
<button
|
|
250
|
-
onClick={() =>
|
|
251
|
-
send({
|
|
252
|
-
type: "profile.edit",
|
|
253
|
-
userId,
|
|
254
|
-
})
|
|
255
|
-
}
|
|
256
|
-
>
|
|
257
|
-
Edit Profile
|
|
258
|
-
</button>
|
|
259
|
-
</div>
|
|
260
|
-
),
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
<PlayRenderer actor={actor} components={components} />;
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
### useSignalEffect for Custom Rendering
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
104
|
import { useSignalEffect } from "@xmachines/play-react";
|
|
270
|
-
import { AbstractActor } from "@xmachines/play-actor";
|
|
271
105
|
|
|
272
|
-
function
|
|
106
|
+
function MyComponent({ actor }) {
|
|
273
107
|
const [view, setView] = useState(null);
|
|
274
108
|
|
|
275
|
-
// Subscribe to currentView signal
|
|
276
109
|
useSignalEffect(() => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
if (!view) return <div>No view</div>;
|
|
110
|
+
setView(actor.currentView.get());
|
|
111
|
+
}, [actor]); // deps: re-subscribe when the actor prop swaps
|
|
282
112
|
|
|
283
|
-
|
|
284
|
-
if (view.component === "SpecialCase") {
|
|
285
|
-
return <SpecialCaseComponent {...view.props} actor={actor} />;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Fallback to standard rendering
|
|
289
|
-
return <DefaultComponent view={view} actor={actor} />;
|
|
113
|
+
return <div>{view?.component}</div>;
|
|
290
114
|
}
|
|
291
115
|
```
|
|
292
116
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
117
|
+
## API Summary
|
|
118
|
+
|
|
119
|
+
### Components
|
|
120
|
+
|
|
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. |
|
|
127
|
+
|
|
128
|
+
### Hooks
|
|
129
|
+
|
|
130
|
+
| Export | Description |
|
|
131
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
132
|
+
| `useSignalEffect(callback, deps?)` | Subscribes to TC39 signal changes; re-runs the callback when any accessed signal changes (re-render via the callback's own setState). Optional `deps` recreates the subscription, like `useEffect`. 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. |
|
|
135
|
+
|
|
136
|
+
### Types
|
|
137
|
+
|
|
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
|
+
| `AnyPlayActor` | Type alias for `AbstractActor<AnyActorLogic>` — the bare actor type used by context providers |
|
|
145
|
+
| `ViewContextValue` | Value shape returned by `usePlayView()` |
|
|
146
|
+
| `RenderErrorHandler` | Error handler callback type for render errors |
|
|
147
|
+
|
|
148
|
+
### Re-exports from `@xmachines/json-render-react`
|
|
149
|
+
|
|
150
|
+
`@xmachines/play-react` re-exports the full `@xmachines/json-render-react` surface so consumers only need one import:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import {
|
|
154
|
+
defineRegistry,
|
|
155
|
+
useBoundProp,
|
|
156
|
+
JSONUIProvider,
|
|
157
|
+
StateProvider,
|
|
158
|
+
ActionProvider,
|
|
159
|
+
VisibilityProvider,
|
|
160
|
+
ValidationProvider,
|
|
161
|
+
Renderer,
|
|
162
|
+
} from "@xmachines/play-react";
|
|
333
163
|
```
|
|
334
164
|
|
|
335
|
-
##
|
|
336
|
-
|
|
337
|
-
This package implements **Signal-Only Reactivity (INV-05)** and **Passive Infrastructure (INV-04)**:
|
|
338
|
-
|
|
339
|
-
1. **No Business Logic in React:**
|
|
340
|
-
- No useState/useReducer for business state
|
|
341
|
-
- No useEffect for side effects
|
|
342
|
-
- React only triggers renders, doesn't control state
|
|
165
|
+
## Key Principle
|
|
343
166
|
|
|
344
|
-
|
|
345
|
-
- `actor.currentView.get()` provides UI structure
|
|
346
|
-
- `actor.currentRoute.get()` provides navigation state
|
|
347
|
-
- Components observe signals via `useSignalEffect`
|
|
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.
|
|
348
168
|
|
|
349
|
-
|
|
350
|
-
- Components receive `send` function via props
|
|
351
|
-
- User actions send events to actor (e.g., `{ type: "auth.login" }`)
|
|
352
|
-
- Actor guards validate and process events
|
|
169
|
+
## Testing
|
|
353
170
|
|
|
354
|
-
|
|
355
|
-
- `Signal.subtle.Watcher` coalesces rapid signal changes
|
|
356
|
-
- Prevents React thrashing from multiple signal updates
|
|
357
|
-
- Single React render per microtask batch
|
|
171
|
+
Run unit tests (jsdom environment):
|
|
358
172
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
**Pattern:**
|
|
364
|
-
|
|
365
|
-
- Renderer receives actor via props (provider pattern)
|
|
366
|
-
- Enables composition with navigation, headers, footers
|
|
367
|
-
- Supports multiple renderers in same app
|
|
368
|
-
|
|
369
|
-
**Architectural Invariants:**
|
|
173
|
+
```bash
|
|
174
|
+
pnpm --filter @xmachines/play-react test
|
|
175
|
+
```
|
|
370
176
|
|
|
371
|
-
|
|
372
|
-
- **Passive Infrastructure (INV-04):** Components reflect, never decide
|
|
177
|
+
Run tests with coverage:
|
|
373
178
|
|
|
374
|
-
|
|
179
|
+
```bash
|
|
180
|
+
pnpm --filter @xmachines/play-react run test:coverage
|
|
181
|
+
```
|
|
375
182
|
|
|
376
|
-
|
|
377
|
-
- **Type Safety:** Props validated against catalog schemas
|
|
378
|
-
- **Simple Testing:** Test actors without React renderer
|
|
379
|
-
- **Performance:** Microtask batching reduces unnecessary renders
|
|
380
|
-
- **Composability:** Renderer prop enables complex layouts ()
|
|
183
|
+
Run browser integration tests (requires Chromium):
|
|
381
184
|
|
|
382
|
-
|
|
185
|
+
```bash
|
|
186
|
+
pnpm --filter @xmachines/play-react run test:browser
|
|
187
|
+
```
|
|
383
188
|
|
|
384
|
-
|
|
385
|
-
- **[@xmachines/play-catalog](../play-catalog)** - UI schema validation
|
|
386
|
-
- **[@xmachines/play-tanstack-react-router](../play-tanstack-react-router)** - TanStack Router integration
|
|
387
|
-
- **[@xmachines/play-actor](../play-actor)** - Actor base
|
|
388
|
-
- **[@xmachines/play-signals](../play-signals)** - TC39 Signals primitives
|
|
189
|
+
Coverage thresholds: 80% lines, functions, branches, and statements.
|
|
389
190
|
|
|
390
191
|
## License
|
|
391
192
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
This work is licensed under the terms of the MIT license.
|
|
395
|
-
For a copy, see <https://opensource.org/licenses/MIT>.
|
|
193
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActorProvider — escape hatch primitive for actor lifecycle management.
|
|
3
|
+
*
|
|
4
|
+
* Owns: actor bridging, signal subscription (useSignalEffect), per-view StateStore
|
|
5
|
+
* lifecycle (controlled/uncontrolled), handler resolution via inner component pattern
|
|
6
|
+
* (uses useStateStore()), StateProvider wrap, PlayErrorBoundary wrap, onRenderError injection.
|
|
7
|
+
*
|
|
8
|
+
* Standard usage: prefer <PlayUIProvider> unless you need to compose providers manually.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
import React from "react";
|
|
13
|
+
import type { DefineRegistryResult, ComponentRegistry } from "@xmachines/json-render-react";
|
|
14
|
+
import { type BaseActorProviderProps, type BaseViewContextValue } from "@xmachines/play-actor";
|
|
15
|
+
/**
|
|
16
|
+
* Props for the ActorProvider component.
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export interface ActorProviderProps extends BaseActorProviderProps<DefineRegistryResult> {
|
|
21
|
+
/** Optional component shown when currentView is null or a catalog component throws */
|
|
22
|
+
fallback?: React.ReactNode;
|
|
23
|
+
/** Optional error handler callback invoked when a catalog component throws during render */
|
|
24
|
+
onError?: (error: Error, info: React.ErrorInfo) => void;
|
|
25
|
+
/** Child components to render inside the provider tree */
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Value provided by ViewContext (accessible via usePlayView()).
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export interface ViewContextValue extends BaseViewContextValue<ComponentRegistry> {
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Hook to access the current view spec, handlers, and registry.
|
|
37
|
+
*
|
|
38
|
+
* Must be called inside <ActorProvider> or <PlayUIProvider>.
|
|
39
|
+
*
|
|
40
|
+
* @throws {Error} If called outside an ActorProvider/PlayUIProvider tree
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import { usePlayView } from "@xmachines/play-react";
|
|
45
|
+
*
|
|
46
|
+
* function MyRenderer() {
|
|
47
|
+
* const view = usePlayView();
|
|
48
|
+
* return <Renderer spec={view.spec} registry={view.registry} />;
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export declare function usePlayView(): ViewContextValue;
|
|
55
|
+
/**
|
|
56
|
+
* ActorProvider — escape hatch primitive for composing actor lifecycle with custom providers.
|
|
57
|
+
*
|
|
58
|
+
* Subscribes to actor.currentView signal, manages the per-view StateStore lifecycle,
|
|
59
|
+
* wraps children in StateProvider and PlayErrorBoundary, and injects onRenderError
|
|
60
|
+
* into the component registry.
|
|
61
|
+
*
|
|
62
|
+
* Standard usage: prefer <PlayUIProvider> unless you need to compose providers manually.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* // Custom composition (escape hatch):
|
|
67
|
+
* <ActorProvider actor={actor} registryResult={registryResult}>
|
|
68
|
+
* <JSONUIProvider registry={registryResult.registry}>
|
|
69
|
+
* <PlayRenderer />
|
|
70
|
+
* </JSONUIProvider>
|
|
71
|
+
* </ActorProvider>
|
|
72
|
+
*
|
|
73
|
+
* // Standard usage: prefer PlayUIProvider
|
|
74
|
+
* <PlayUIProvider actor={actor} registryResult={registryResult}>
|
|
75
|
+
* <PlayRenderer />
|
|
76
|
+
* </PlayUIProvider>
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @public
|
|
80
|
+
*/
|
|
81
|
+
export declare const ActorProvider: React.FC<ActorProviderProps>;
|
|
82
|
+
//# sourceMappingURL=ActorProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActorProvider.d.ts","sourceRoot":"","sources":["../src/ActorProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAA+D,MAAM,OAAO,CAAC;AAEpF,OAAO,KAAK,EACX,oBAAoB,EAEpB,iBAAiB,EACjB,MAAM,8BAA8B,CAAC;AAOtC,OAAO,EAIN,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,MAAM,uBAAuB,CAAC;AAI/B;;;;GAIG;AACH,MAAM,WAAW,kBAAmB,SAAQ,sBAAsB,CAAC,oBAAoB,CAAC;IACvF,sFAAsF;IACtF,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,4FAA4F;IAC5F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;IACxD,0DAA0D;IAC1D,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAiB,SAAQ,oBAAoB,CAAC,iBAAiB,CAAC;CAAG;AAQpF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,IAAI,gBAAgB,CAE9C;AA0ED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAqFtD,CAAC"}
|