@xmachines/play-react 1.0.0-beta.15 → 1.0.0-beta.17
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 +212 -304
- 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 +19 -28
- package/dist/PlayRenderer.d.ts.map +1 -1
- package/dist/PlayRenderer.js +60 -45
- package/dist/PlayRenderer.js.map +1 -1
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +36 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/useActor.d.ts +26 -0
- package/dist/useActor.d.ts.map +1 -0
- package/dist/useActor.js +29 -0
- package/dist/useActor.js.map +1 -0
- package/package.json +15 -6
package/README.md
CHANGED
|
@@ -1,393 +1,301 @@
|
|
|
1
1
|
# @xmachines/play-react
|
|
2
2
|
|
|
3
|
-
**React renderer
|
|
3
|
+
**React renderer for XMachines Play Architecture**
|
|
4
4
|
|
|
5
|
-
Signal-driven React
|
|
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
|
|
9
|
+
`@xmachines/play-react` provides `PlayRenderer`, a React component that:
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
- **Passive Infrastructure (INV-04):** Components observe signals, send events to actor
|
|
16
|
+
Per [RFC Play v1](https://gitlab.com/xmachin-es/rfc/-/blob/main/src/play-v1.md):
|
|
15
17
|
|
|
16
|
-
**
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
|
27
30
|
## Current Exports
|
|
28
31
|
|
|
29
|
-
- `PlayRenderer`
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
32
|
+
- `PlayRenderer` — main renderer component
|
|
33
|
+
- `useActor` — hook for accessing the actor inside a `PlayRenderer` tree
|
|
34
|
+
- `useSignalEffect` — React hook for subscribing to TC39 Signals
|
|
35
|
+
- `PlayErrorBoundary` — error boundary wrapping renderer output
|
|
36
|
+
- `defineRegistry` — re-exported from `@json-render/react`
|
|
37
|
+
- `useStateBinding` — re-exported from `@json-render/react`
|
|
38
|
+
- `ComponentFn` (type) — re-exported from `@json-render/react`
|
|
39
|
+
- `ComponentContext` (type) — re-exported from `@json-render/react`
|
|
32
40
|
- `PlayRendererProps` (type)
|
|
33
|
-
- `
|
|
34
|
-
|
|
35
|
-
**Peer dependencies:**
|
|
36
|
-
|
|
37
|
-
- `react` ^18.0.0 || ^19.0.0 — React runtime
|
|
38
|
-
- `react-dom` ^18.0.0 || ^19.0.0 — React DOM renderer
|
|
41
|
+
- `PlayActor` (type)
|
|
39
42
|
|
|
40
43
|
## Quick Start
|
|
41
44
|
|
|
42
|
-
```
|
|
43
|
-
import {
|
|
44
|
-
import { definePlayer } from "@xmachines/play-xstate";
|
|
45
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
45
|
+
```tsx
|
|
46
|
+
import { definePlayer, formatPlayRouteTransitions } from "@xmachines/play-xstate";
|
|
46
47
|
import { PlayRenderer } from "@xmachines/play-react";
|
|
48
|
+
import { defineCatalog } from "@json-render/core";
|
|
49
|
+
import { defineRegistry } from "@xmachines/play-react";
|
|
50
|
+
import type { ComponentFn } from "@xmachines/play-react";
|
|
51
|
+
import { setup, assign } from "xstate";
|
|
47
52
|
import { z } from "zod";
|
|
48
53
|
|
|
49
|
-
// 1. Define catalog
|
|
54
|
+
// 1. Define catalog — the contract between machine spec and UI components
|
|
50
55
|
const catalog = defineCatalog({
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}),
|
|
56
|
+
elements: {
|
|
57
|
+
Login: { props: z.object({ title: z.string() }), description: "Login form" },
|
|
58
|
+
Dashboard: { props: z.object({ username: z.string() }), description: "Dashboard" },
|
|
59
|
+
},
|
|
56
60
|
});
|
|
57
61
|
|
|
58
|
-
// 2.
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
62
|
+
// 2. Implement components using ComponentFn — typed against catalog entries
|
|
63
|
+
const Login: ComponentFn<typeof catalog, "Login"> = ({ props, emit }) => (
|
|
64
|
+
<form
|
|
65
|
+
onSubmit={(e) => {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
emit("submit");
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<h2>{props.title}</h2>
|
|
71
|
+
<button type="submit">Log In</button>
|
|
72
|
+
</form>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const Dashboard: ComponentFn<typeof catalog, "Dashboard"> = ({ props }) => (
|
|
76
|
+
<div>Welcome, {props.username}!</div>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// 3. Build registry — wires components to catalog and declares no-op action stubs
|
|
80
|
+
const { registry } = defineRegistry(catalog, {
|
|
81
|
+
components: { Login, Dashboard },
|
|
82
|
+
actions: { login: async () => {}, logout: async () => {} },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 4. Define machine with view metadata
|
|
86
|
+
const machine = setup({
|
|
87
|
+
types: {
|
|
88
|
+
context: {} as {
|
|
89
|
+
isAuthenticated: boolean;
|
|
90
|
+
username: string | null;
|
|
91
|
+
routeParams: Record<string, string>;
|
|
92
|
+
queryParams: Record<string, string>;
|
|
93
|
+
},
|
|
94
|
+
events: {} as
|
|
95
|
+
| { type: "auth.login"; username: string }
|
|
96
|
+
| { type: "auth.logout" }
|
|
97
|
+
| { type: "play.route"; to: string; params?: Record<string, string> },
|
|
98
|
+
},
|
|
99
|
+
}).createMachine(
|
|
100
|
+
formatPlayRouteTransitions({
|
|
101
|
+
id: "app",
|
|
102
|
+
initial: "login",
|
|
103
|
+
context: { isAuthenticated: false, username: null, routeParams: {}, queryParams: {} },
|
|
104
|
+
states: {
|
|
105
|
+
login: {
|
|
106
|
+
id: "login",
|
|
107
|
+
meta: {
|
|
108
|
+
route: "/login",
|
|
109
|
+
view: {
|
|
110
|
+
component: "Login",
|
|
111
|
+
spec: {
|
|
112
|
+
root: "root",
|
|
113
|
+
elements: {
|
|
114
|
+
root: { type: "Login", props: { title: "Sign In" }, children: [] },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
dashboard: {
|
|
121
|
+
id: "dashboard",
|
|
122
|
+
meta: {
|
|
123
|
+
route: "/dashboard",
|
|
124
|
+
view: {
|
|
125
|
+
component: "Dashboard",
|
|
126
|
+
spec: {
|
|
127
|
+
root: "root",
|
|
128
|
+
elements: {
|
|
129
|
+
root: { type: "Dashboard", props: { username: "" }, children: [] },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
on: {
|
|
137
|
+
"auth.login": {
|
|
138
|
+
target: ".dashboard",
|
|
139
|
+
guard: ({ context }) => !context.isAuthenticated,
|
|
140
|
+
actions: assign({
|
|
141
|
+
isAuthenticated: true,
|
|
142
|
+
username: ({ event }) => event.username,
|
|
143
|
+
}),
|
|
144
|
+
},
|
|
145
|
+
"auth.logout": {
|
|
146
|
+
target: ".login",
|
|
147
|
+
guard: ({ context }) => context.isAuthenticated,
|
|
148
|
+
actions: assign({ isAuthenticated: false, username: null }),
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// 5. Create actor and render
|
|
155
|
+
const createPlayer = definePlayer({ machine });
|
|
87
156
|
const actor = createPlayer();
|
|
88
157
|
actor.start();
|
|
89
158
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
159
|
+
function App() {
|
|
160
|
+
return (
|
|
161
|
+
<PlayRenderer
|
|
162
|
+
actor={actor}
|
|
163
|
+
registry={registry}
|
|
164
|
+
actions={{ login: "auth.login", logout: "auth.logout" }}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
93
168
|
```
|
|
94
169
|
|
|
95
170
|
## API Reference
|
|
96
171
|
|
|
97
|
-
### PlayRenderer
|
|
172
|
+
### `PlayRenderer`
|
|
98
173
|
|
|
99
|
-
Main
|
|
174
|
+
Main component. Subscribes to `actor.currentView` and renders the spec.
|
|
100
175
|
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
actor
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
176
|
+
```tsx
|
|
177
|
+
<PlayRenderer
|
|
178
|
+
actor={actor} // required
|
|
179
|
+
registry={registry} // required
|
|
180
|
+
actions={{ login: "auth.login" }} // optional
|
|
181
|
+
store={myStore} // optional — controlled mode
|
|
182
|
+
fallback={<p>Loading…</p>} // optional
|
|
183
|
+
/>
|
|
107
184
|
```
|
|
108
185
|
|
|
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:**
|
|
186
|
+
**`actor`** — A `PlayerActor` (or any `AbstractActor & Viewable`). Provides the `currentView` signal.
|
|
116
187
|
|
|
117
|
-
|
|
118
|
-
2. Looks up component from `components` map using `view.component` string
|
|
119
|
-
3. Renders component with props from `view.props` + `send` function
|
|
188
|
+
**`registry`** — Built with `defineRegistry(catalog, { components, actions })` from `@xmachines/play-react`.
|
|
120
189
|
|
|
121
|
-
|
|
190
|
+
**`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
191
|
|
|
123
|
-
```
|
|
124
|
-
|
|
192
|
+
```tsx
|
|
193
|
+
// Typed: "bad.event" causes a compile error if it is not in the machine's event union
|
|
194
|
+
<PlayRenderer<typeof machine>
|
|
125
195
|
actor={actor}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
AboutPage: ({ send }) => <div>About</div>,
|
|
129
|
-
}}
|
|
130
|
-
fallback={<div>Loading...</div>}
|
|
196
|
+
registry={registry}
|
|
197
|
+
actions={{ login: "auth.login", logout: "auth.logout" }}
|
|
131
198
|
/>
|
|
132
199
|
```
|
|
133
200
|
|
|
134
|
-
|
|
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
|
-
});
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**Behavior:**
|
|
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()`
|
|
201
|
+
**`store`** (optional) — Controls per-view UI state (form values, `$state` bindings):
|
|
159
202
|
|
|
160
|
-
|
|
203
|
+
- **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.
|
|
204
|
+
- **Provided (controlled):** The caller owns the store lifecycle. `spec.state` is ignored.
|
|
161
205
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
import {
|
|
166
|
-
import { useState } from "react";
|
|
167
|
-
|
|
168
|
-
function RouteDisplay({ actor }: { actor: AbstractActor<any> }) {
|
|
169
|
-
const [route, setRoute] = useState<string | null>(null);
|
|
206
|
+
```tsx
|
|
207
|
+
import { createAtom } from "@xstate/store";
|
|
208
|
+
import { xstateStoreStateStore } from "@json-render/xstate";
|
|
209
|
+
import type { StateStore } from "@json-render/core";
|
|
170
210
|
|
|
171
|
-
|
|
172
|
-
const currentRoute = actor.currentRoute.get();
|
|
173
|
-
setRoute(currentRoute);
|
|
174
|
-
});
|
|
211
|
+
const store: StateStore = xstateStoreStateStore({ atom: createAtom({ username: "alice" }) });
|
|
175
212
|
|
|
176
|
-
|
|
177
|
-
}
|
|
213
|
+
<PlayRenderer actor={actor} registry={registry} store={store} actions={{ login: "auth.login" }} />;
|
|
178
214
|
```
|
|
179
215
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
React class component error boundary for catching catalog component render errors.
|
|
183
|
-
|
|
184
|
-
`PlayRenderer` wraps its render output in `PlayErrorBoundary` automatically. You can also use it directly to wrap any component that may throw during render.
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
interface PlayErrorBoundaryProps {
|
|
188
|
-
fallback?: React.ReactNode; // UI shown when a child throws (default: null)
|
|
189
|
-
children: React.ReactNode;
|
|
190
|
-
onError?: (error: Error, info: React.ErrorInfo) => void; // Forward to Sentry, Datadog, etc.
|
|
191
|
-
}
|
|
192
|
-
```
|
|
216
|
+
**`fallback`** — Shown when `actor.currentView.get()` is `null` (machine in a no-view state, or during initialisation).
|
|
193
217
|
|
|
194
|
-
|
|
218
|
+
---
|
|
195
219
|
|
|
196
|
-
|
|
197
|
-
- `onError` — Optional callback forwarded on every caught error. Use for production observability (Sentry, Datadog, custom logging).
|
|
220
|
+
### `useActor`
|
|
198
221
|
|
|
199
|
-
|
|
222
|
+
Hook for accessing the actor from inside any component rendered by `PlayRenderer`. No prop drilling needed.
|
|
200
223
|
|
|
201
224
|
```tsx
|
|
202
|
-
import {
|
|
225
|
+
import { useActor } from "@xmachines/play-react";
|
|
203
226
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
>
|
|
208
|
-
|
|
209
|
-
</PlayErrorBoundary>;
|
|
227
|
+
// Works in any component rendered inside PlayRenderer:
|
|
228
|
+
function LogoutButton() {
|
|
229
|
+
const actor = useActor();
|
|
230
|
+
return <button onClick={() => actor.send({ type: "auth.logout" })}>Log Out</button>;
|
|
231
|
+
}
|
|
210
232
|
```
|
|
211
233
|
|
|
212
|
-
|
|
234
|
+
Throws `"useActor() must be called inside <PlayRenderer>"` if called outside the tree.
|
|
213
235
|
|
|
214
|
-
|
|
236
|
+
---
|
|
215
237
|
|
|
216
|
-
###
|
|
238
|
+
### `useSignalEffect`
|
|
217
239
|
|
|
218
|
-
|
|
219
|
-
import { PlayRenderer } from "@xmachines/play-react";
|
|
220
|
-
import { defineCatalog } from "@xmachines/play-catalog";
|
|
221
|
-
import { z } from "zod";
|
|
222
|
-
|
|
223
|
-
// Define schema in catalog
|
|
224
|
-
const catalog = defineCatalog({
|
|
225
|
-
UserProfile: z.object({
|
|
226
|
-
userId: z.string(),
|
|
227
|
-
name: z.string(),
|
|
228
|
-
avatar: z.string().url().optional(),
|
|
229
|
-
stats: z.object({
|
|
230
|
-
posts: z.number(),
|
|
231
|
-
followers: z.number(),
|
|
232
|
-
}),
|
|
233
|
-
}),
|
|
234
|
-
});
|
|
240
|
+
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).
|
|
235
241
|
|
|
236
|
-
|
|
237
|
-
const components = {
|
|
238
|
-
UserProfile: ({ userId, name, avatar, stats, send }) => (
|
|
239
|
-
<div>
|
|
240
|
-
{avatar && <img src={avatar} alt={name} />}
|
|
241
|
-
<h1>{name}</h1>
|
|
242
|
-
<p>ID: {userId}</p>
|
|
243
|
-
<div>
|
|
244
|
-
<span>{stats.posts} posts</span>
|
|
245
|
-
<span>{stats.followers} followers</span>
|
|
246
|
-
</div>
|
|
247
|
-
<button
|
|
248
|
-
onClick={() =>
|
|
249
|
-
send({
|
|
250
|
-
type: "profile.edit",
|
|
251
|
-
userId,
|
|
252
|
-
})
|
|
253
|
-
}
|
|
254
|
-
>
|
|
255
|
-
Edit Profile
|
|
256
|
-
</button>
|
|
257
|
-
</div>
|
|
258
|
-
),
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
<PlayRenderer actor={actor} components={components} />;
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### useSignalEffect for Custom Rendering
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
242
|
+
```tsx
|
|
267
243
|
import { useSignalEffect } from "@xmachines/play-react";
|
|
268
|
-
import { AbstractActor } from "@xmachines/play-actor";
|
|
269
244
|
|
|
270
|
-
function
|
|
271
|
-
const [
|
|
245
|
+
function NavBar({ actor }: { actor: ReturnType<typeof createPlayer> }) {
|
|
246
|
+
const [isAuth, setIsAuth] = useState(false);
|
|
272
247
|
|
|
273
|
-
// Subscribe to currentView signal
|
|
274
248
|
useSignalEffect(() => {
|
|
275
|
-
const
|
|
276
|
-
|
|
249
|
+
const snap = actor.state.get();
|
|
250
|
+
setIsAuth((snap.context as { isAuthenticated: boolean }).isAuthenticated);
|
|
277
251
|
});
|
|
278
252
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// Custom rendering logic
|
|
282
|
-
if (view.component === "SpecialCase") {
|
|
283
|
-
return <SpecialCaseComponent {...view.props} actor={actor} />;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Fallback to standard rendering
|
|
287
|
-
return <DefaultComponent view={view} actor={actor} />;
|
|
253
|
+
return <nav>{isAuth ? <LogoutBtn /> : <LoginBtn />}</nav>;
|
|
288
254
|
}
|
|
289
255
|
```
|
|
290
256
|
|
|
291
|
-
|
|
257
|
+
---
|
|
292
258
|
|
|
293
|
-
|
|
294
|
-
import { PlayTanStackRouterProvider } from "@xmachines/play-tanstack-react-router";
|
|
295
|
-
import { PlayRenderer } from "@xmachines/play-react";
|
|
259
|
+
### `PlayErrorBoundary`
|
|
296
260
|
|
|
297
|
-
|
|
298
|
-
function App() {
|
|
299
|
-
return (
|
|
300
|
-
<PlayTanStackRouterProvider
|
|
301
|
-
actor={actor}
|
|
302
|
-
router={router}
|
|
303
|
-
renderer={(currentActor, currentRouter) => {
|
|
304
|
-
void currentRouter;
|
|
305
|
-
return (
|
|
306
|
-
<div>
|
|
307
|
-
<Header actor={currentActor} />
|
|
308
|
-
<PlayRenderer actor={currentActor} components={components} />
|
|
309
|
-
<Footer />
|
|
310
|
-
</div>
|
|
311
|
-
);
|
|
312
|
-
}}
|
|
313
|
-
/>
|
|
314
|
-
);
|
|
315
|
-
}
|
|
261
|
+
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.
|
|
316
262
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const [route, setRoute] = useState<string | null>(null);
|
|
320
|
-
|
|
321
|
-
useSignalEffect(() => {
|
|
322
|
-
setRoute(actor.currentRoute.get());
|
|
323
|
-
});
|
|
263
|
+
```tsx
|
|
264
|
+
import { PlayErrorBoundary } from "@xmachines/play-react";
|
|
324
265
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
</header>
|
|
329
|
-
);
|
|
330
|
-
}
|
|
266
|
+
<PlayErrorBoundary fallback={<p>Something went wrong.</p>}>
|
|
267
|
+
<PlayRenderer actor={actor} registry={registry} />
|
|
268
|
+
</PlayErrorBoundary>;
|
|
331
269
|
```
|
|
332
270
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
This package implements **Signal-Only Reactivity (INV-05)** and **Passive Infrastructure (INV-04)**:
|
|
336
|
-
|
|
337
|
-
1. **No Business Logic in React:**
|
|
338
|
-
- No useState/useReducer for business state
|
|
339
|
-
- No useEffect for side effects
|
|
340
|
-
- React only triggers renders, doesn't control state
|
|
341
|
-
|
|
342
|
-
2. **Signals as Source of Truth:**
|
|
343
|
-
- `actor.currentView.get()` provides UI structure
|
|
344
|
-
- `actor.currentRoute.get()` provides navigation state
|
|
345
|
-
- Components observe signals via `useSignalEffect`
|
|
271
|
+
---
|
|
346
272
|
|
|
347
|
-
|
|
348
|
-
- Components receive `send` function via props
|
|
349
|
-
- User actions send events to actor (e.g., `{ type: "auth.login" }`)
|
|
350
|
-
- Actor guards validate and process events
|
|
273
|
+
## Route Parameters in Props
|
|
351
274
|
|
|
352
|
-
|
|
353
|
-
- `Signal.subtle.Watcher` coalesces rapid signal changes
|
|
354
|
-
- Prevents React thrashing from multiple signal updates
|
|
355
|
-
- Single React render per microtask batch
|
|
275
|
+
When using `formatPlayRouteTransitions`, URL path parameters flow automatically into component props. Declare an `undefined` slot in the spec to opt in:
|
|
356
276
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
- Renderer receives actor via props (provider pattern)
|
|
364
|
-
- Enables composition with navigation, headers, footers
|
|
365
|
-
- Supports multiple renderers in same app
|
|
366
|
-
|
|
367
|
-
**Architectural Invariants:**
|
|
368
|
-
|
|
369
|
-
- **Signal-Only Reactivity (INV-05):** No React state for business logic
|
|
370
|
-
- **Passive Infrastructure (INV-04):** Components reflect, never decide
|
|
277
|
+
```ts
|
|
278
|
+
// spec: { section: undefined, user: "alice" }
|
|
279
|
+
// After play.route to /settings/profile → context.routeParams = { section: "profile" }
|
|
280
|
+
// Component receives: { section: "profile", user: "alice" }
|
|
281
|
+
```
|
|
371
282
|
|
|
372
|
-
|
|
283
|
+
Priority: **route param fills `undefined` slots; explicit non-`undefined` spec props always win.**
|
|
373
284
|
|
|
374
|
-
|
|
375
|
-
- **Type Safety:** Props validated against catalog schemas
|
|
376
|
-
- **Simple Testing:** Test actors without React renderer
|
|
377
|
-
- **Performance:** Microtask batching reduces unnecessary renders
|
|
378
|
-
- **Composability:** Renderer prop enables complex layouts ()
|
|
285
|
+
---
|
|
379
286
|
|
|
380
|
-
##
|
|
287
|
+
## Error Handling
|
|
381
288
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
- **[@xmachines/play-signals](../play-signals)** - TC39 Signals primitives
|
|
289
|
+
| Error | Cause | Fix |
|
|
290
|
+
| ------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------- |
|
|
291
|
+
| `useActor() must be called inside <PlayRenderer>` | Hook called outside the tree | Move inside a component rendered by `PlayRenderer` |
|
|
292
|
+
| Component render error | Component throws | `PlayErrorBoundary` catches it; inspect component implementation |
|
|
387
293
|
|
|
388
|
-
|
|
294
|
+
---
|
|
389
295
|
|
|
390
|
-
|
|
296
|
+
## Architecture Notes
|
|
391
297
|
|
|
392
|
-
|
|
393
|
-
|
|
298
|
+
- React `useState` is **only** used to trigger re-renders — never for business logic
|
|
299
|
+
- `actor.currentView` (TC39 Signal) is the sole render trigger; `PlayRenderer` is a passive observer
|
|
300
|
+
- Per-view UI state lives in an `@xstate/store` atom, not in React state
|
|
301
|
+
- `@json-render/react` drives rendering; `PlayRenderer` is the signal bridge — import `defineRegistry`, `ComponentFn`, `ComponentContext`, and `useStateBinding` from `@xmachines/play-react`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlayErrorBoundary.d.ts","sourceRoot":"","sources":["../src/PlayErrorBoundary.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"PlayErrorBoundary.d.ts","sourceRoot":"","sources":["../src/PlayErrorBoundary.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,iCAAiC;IACjC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,8FAA8F;IAC9F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;CACxD;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,iBAAkB,SAAQ,KAAK,CAAC,SAAS,CACrD,sBAAsB,EACtB,sBAAsB,CACtB;gBACY,KAAK,EAAE,sBAAsB;IAKzC,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,sBAAsB;IAI5D,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,SAAS,GAAG,IAAI;IAO5D,MAAM,IAAI,KAAK,CAAC,SAAS;CAMlC"}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
6
|
import React from "react";
|
|
7
|
+
import { RendererError } from "./errors.js";
|
|
7
8
|
/**
|
|
8
9
|
* React class component error boundary for catching catalog component render errors.
|
|
9
10
|
*
|
|
@@ -29,8 +30,10 @@ export class PlayErrorBoundary extends React.Component {
|
|
|
29
30
|
return { hasError: true, error };
|
|
30
31
|
}
|
|
31
32
|
componentDidCatch(error, info) {
|
|
32
|
-
console.error("[PlayErrorBoundary] Component render error:", error, info);
|
|
33
33
|
this.props.onError?.(error, info);
|
|
34
|
+
throw new RendererError("Component render error caught by PlayErrorBoundary.", {
|
|
35
|
+
cause: error,
|
|
36
|
+
});
|
|
34
37
|
}
|
|
35
38
|
render() {
|
|
36
39
|
if (this.state.hasError) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlayErrorBoundary.js","sourceRoot":"","sources":["../src/PlayErrorBoundary.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"PlayErrorBoundary.js","sourceRoot":"","sources":["../src/PlayErrorBoundary.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA0B5C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK,CAAC,SAG5C;IACA,YAAY,KAA6B;QACxC,KAAK,CAAC,KAAK,CAAC,CAAC;QACb,IAAI,CAAC,KAAK,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/C,CAAC;IAED,MAAM,CAAC,wBAAwB,CAAC,KAAY;QAC3C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAClC,CAAC;IAEQ,iBAAiB,CAAC,KAAY,EAAE,IAAqB;QAC7D,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAClC,MAAM,IAAI,aAAa,CAAC,qDAAqD,EAAE;YAC9E,KAAK,EAAE,KAAK;SACZ,CAAC,CAAC;IACJ,CAAC;IAEQ,MAAM;QACd,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC;QACpC,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;IAC5B,CAAC;CACD"}
|
package/dist/PlayRenderer.d.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PlayRenderer - Main React renderer component for XMachines Play architecture
|
|
3
3
|
*
|
|
4
|
+
* Backed by @json-render/react for spec-driven UI rendering.
|
|
5
|
+
*
|
|
4
6
|
* @packageDocumentation
|
|
5
7
|
*/
|
|
6
8
|
import React from "react";
|
|
7
9
|
import type { PlayRendererProps } from "./types.js";
|
|
8
10
|
/**
|
|
9
11
|
* Main renderer component that subscribes to actor signals and renders UI
|
|
12
|
+
* via @json-render/react Renderer.
|
|
10
13
|
*
|
|
11
|
-
* Architecture
|
|
14
|
+
* Architecture:
|
|
12
15
|
* - Subscribes to actor.currentView signal via useSignalEffect
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
+
* - Renders view.spec via StateProvider → ActionProvider → VisibilityProvider → Renderer
|
|
17
|
+
* - Routes action names to actor.send() via the `actions` prop mapping
|
|
18
|
+
* - State store: uses external `store` prop if provided (controlled mode); otherwise
|
|
19
|
+
* creates a fresh @xstate/store atom per view transition seeded from spec.state.
|
|
20
|
+
* The atom resets automatically when the actor transitions to a new view, mirroring
|
|
21
|
+
* the actor's currentView lifecycle.
|
|
16
22
|
*
|
|
17
23
|
* Invariant: Actor Authority - Actor decides all state transitions via guards.
|
|
18
24
|
* Invariant: Passive Infrastructure - Component observes signals and sends events.
|
|
@@ -21,37 +27,22 @@ import type { PlayRendererProps } from "./types.js";
|
|
|
21
27
|
* @example
|
|
22
28
|
* ```typescript
|
|
23
29
|
* import { PlayRenderer } from "@xmachines/play-react";
|
|
24
|
-
* import {
|
|
30
|
+
* import { defineRegistry } from "@json-render/react";
|
|
25
31
|
*
|
|
26
|
-
* const
|
|
27
|
-
* actor.start();
|
|
32
|
+
* const { registry } = defineRegistry(catalog, { components: { ... } });
|
|
28
33
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* LoginForm: ({ error, send }) => <form onSubmit={(e) => {
|
|
32
|
-
* e.preventDefault();
|
|
33
|
-
* send({ type: "intent", name: "login.submit", payload: {...} });
|
|
34
|
-
* }}>...</form>
|
|
35
|
-
* };
|
|
34
|
+
* // Uncontrolled — fresh atom created per view, seeded from spec.state:
|
|
35
|
+
* <PlayRenderer actor={actor} registry={registry} actions={{ login: "auth.login" }} />
|
|
36
36
|
*
|
|
37
|
-
*
|
|
37
|
+
* // Controlled — caller provides and owns the store:
|
|
38
|
+
* import { createAtom } from "@xstate/store";
|
|
39
|
+
* import { xstateStoreStateStore } from "@json-render/xstate";
|
|
40
|
+
* const store = xstateStoreStateStore({ atom: createAtom({ username: "" }) });
|
|
41
|
+
* <PlayRenderer actor={actor} registry={registry} store={store} actions={{ login: "auth.login" }} />
|
|
38
42
|
* ```
|
|
39
43
|
*
|
|
40
44
|
* @param props - Component props
|
|
41
45
|
* @returns React element rendering current view from actor
|
|
42
|
-
*
|
|
43
|
-
* @remarks
|
|
44
|
-
* **Component lookup:** Dynamically looks up component from `components` map
|
|
45
|
-
* using `view.component` string from actor.currentView signal.
|
|
46
|
-
*
|
|
47
|
-
* **Event forwarding:** Injects `send` function as prop to components. Components
|
|
48
|
-
* call `send(event)` to forward intents to actor. Actor guards decide validity.
|
|
49
|
-
*
|
|
50
|
-
* **Error handling:** If component not found in catalog, logs error and shows
|
|
51
|
-
* fallback. This indicates missing component registration, not runtime error.
|
|
52
|
-
*
|
|
53
|
-
* **CRITICAL:** Never call actor.send() during render - only in event handlers.
|
|
54
|
-
* Calling send during render causes infinite render loops.
|
|
55
46
|
*/
|
|
56
47
|
export declare const PlayRenderer: React.FC<PlayRendererProps>;
|
|
57
48
|
//# sourceMappingURL=PlayRenderer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlayRenderer.d.ts","sourceRoot":"","sources":["../src/PlayRenderer.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"PlayRenderer.d.ts","sourceRoot":"","sources":["../src/PlayRenderer.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA2B,MAAM,OAAO,CAAC;AAOhD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAYpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAoEpD,CAAC"}
|
package/dist/PlayRenderer.js
CHANGED
|
@@ -2,19 +2,36 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* PlayRenderer - Main React renderer component for XMachines Play architecture
|
|
4
4
|
*
|
|
5
|
+
* Backed by @json-render/react for spec-driven UI rendering.
|
|
6
|
+
*
|
|
5
7
|
* @packageDocumentation
|
|
6
8
|
*/
|
|
7
|
-
import React, { useState } from "react";
|
|
9
|
+
import React, { useState, useRef } from "react";
|
|
10
|
+
import { Renderer, StateProvider, ActionProvider, VisibilityProvider } from "@json-render/react";
|
|
11
|
+
import { createAtom } from "@xstate/store";
|
|
12
|
+
import { xstateStoreStateStore } from "@json-render/xstate";
|
|
8
13
|
import { useSignalEffect } from "./useSignalEffect.js";
|
|
9
14
|
import { PlayErrorBoundary } from "./PlayErrorBoundary.js";
|
|
15
|
+
import { ActorContext } from "./useActor.js";
|
|
16
|
+
/**
|
|
17
|
+
* Create a StateStore backed by a fresh @xstate/store atom seeded from the given state.
|
|
18
|
+
* Called internally per view transition when no external store prop is provided.
|
|
19
|
+
*/
|
|
20
|
+
function createViewStore(initialState) {
|
|
21
|
+
return xstateStoreStateStore({ atom: createAtom(initialState) });
|
|
22
|
+
}
|
|
10
23
|
/**
|
|
11
24
|
* Main renderer component that subscribes to actor signals and renders UI
|
|
25
|
+
* via @json-render/react Renderer.
|
|
12
26
|
*
|
|
13
|
-
* Architecture
|
|
27
|
+
* Architecture:
|
|
14
28
|
* - Subscribes to actor.currentView signal via useSignalEffect
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
29
|
+
* - Renders view.spec via StateProvider → ActionProvider → VisibilityProvider → Renderer
|
|
30
|
+
* - Routes action names to actor.send() via the `actions` prop mapping
|
|
31
|
+
* - State store: uses external `store` prop if provided (controlled mode); otherwise
|
|
32
|
+
* creates a fresh @xstate/store atom per view transition seeded from spec.state.
|
|
33
|
+
* The atom resets automatically when the actor transitions to a new view, mirroring
|
|
34
|
+
* the actor's currentView lifecycle.
|
|
18
35
|
*
|
|
19
36
|
* Invariant: Actor Authority - Actor decides all state transitions via guards.
|
|
20
37
|
* Invariant: Passive Infrastructure - Component observes signals and sends events.
|
|
@@ -23,66 +40,64 @@ import { PlayErrorBoundary } from "./PlayErrorBoundary.js";
|
|
|
23
40
|
* @example
|
|
24
41
|
* ```typescript
|
|
25
42
|
* import { PlayRenderer } from "@xmachines/play-react";
|
|
26
|
-
* import {
|
|
43
|
+
* import { defineRegistry } from "@json-render/react";
|
|
27
44
|
*
|
|
28
|
-
* const
|
|
29
|
-
* actor.start();
|
|
45
|
+
* const { registry } = defineRegistry(catalog, { components: { ... } });
|
|
30
46
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* LoginForm: ({ error, send }) => <form onSubmit={(e) => {
|
|
34
|
-
* e.preventDefault();
|
|
35
|
-
* send({ type: "intent", name: "login.submit", payload: {...} });
|
|
36
|
-
* }}>...</form>
|
|
37
|
-
* };
|
|
47
|
+
* // Uncontrolled — fresh atom created per view, seeded from spec.state:
|
|
48
|
+
* <PlayRenderer actor={actor} registry={registry} actions={{ login: "auth.login" }} />
|
|
38
49
|
*
|
|
39
|
-
*
|
|
50
|
+
* // Controlled — caller provides and owns the store:
|
|
51
|
+
* import { createAtom } from "@xstate/store";
|
|
52
|
+
* import { xstateStoreStateStore } from "@json-render/xstate";
|
|
53
|
+
* const store = xstateStoreStateStore({ atom: createAtom({ username: "" }) });
|
|
54
|
+
* <PlayRenderer actor={actor} registry={registry} store={store} actions={{ login: "auth.login" }} />
|
|
40
55
|
* ```
|
|
41
56
|
*
|
|
42
57
|
* @param props - Component props
|
|
43
58
|
* @returns React element rendering current view from actor
|
|
44
|
-
*
|
|
45
|
-
* @remarks
|
|
46
|
-
* **Component lookup:** Dynamically looks up component from `components` map
|
|
47
|
-
* using `view.component` string from actor.currentView signal.
|
|
48
|
-
*
|
|
49
|
-
* **Event forwarding:** Injects `send` function as prop to components. Components
|
|
50
|
-
* call `send(event)` to forward intents to actor. Actor guards decide validity.
|
|
51
|
-
*
|
|
52
|
-
* **Error handling:** If component not found in catalog, logs error and shows
|
|
53
|
-
* fallback. This indicates missing component registration, not runtime error.
|
|
54
|
-
*
|
|
55
|
-
* **CRITICAL:** Never call actor.send() during render - only in event handlers.
|
|
56
|
-
* Calling send during render causes infinite render loops.
|
|
57
59
|
*/
|
|
58
|
-
export const PlayRenderer = ({ actor,
|
|
60
|
+
export const PlayRenderer = ({ actor, registry, store: externalStore, fallback = null, actions = {}, }) => {
|
|
59
61
|
// React state for triggering re-renders (NOT business logic state)
|
|
60
62
|
// Signal is source of truth, useState is just React's render trigger
|
|
61
63
|
const [view, setView] = useState(() => actor.currentView.get());
|
|
64
|
+
// Internal store ref — tracks the current per-view atom store.
|
|
65
|
+
// Keyed to view identity: recreated whenever the view changes (new spec.state seed).
|
|
66
|
+
// Ignored when externalStore is provided.
|
|
67
|
+
const internalStoreRef = useRef(null);
|
|
68
|
+
const lastViewRef = useRef(null);
|
|
62
69
|
// Subscribe to signal changes
|
|
63
70
|
useSignalEffect(() => {
|
|
64
71
|
const currentView = actor.currentView.get();
|
|
65
72
|
setView(currentView);
|
|
66
73
|
});
|
|
67
|
-
// No view in current state
|
|
74
|
+
// No view in current state — render fallback
|
|
68
75
|
if (!view) {
|
|
69
76
|
return _jsx(_Fragment, { children: fallback });
|
|
70
77
|
}
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
// Resolve the store to use for StateProvider:
|
|
79
|
+
// - External (controlled): use as-is, caller manages lifecycle
|
|
80
|
+
// - Internal: create a fresh atom when the view changes (new route/state)
|
|
81
|
+
let store;
|
|
82
|
+
if (externalStore) {
|
|
83
|
+
store = externalStore;
|
|
76
84
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
else {
|
|
86
|
+
// Recreate the internal store when the view identity changes
|
|
87
|
+
// (view is a new object on every transition per deriveCurrentView)
|
|
88
|
+
if (internalStoreRef.current === null || lastViewRef.current !== view) {
|
|
89
|
+
const initialState = view.spec?.state ?? {};
|
|
90
|
+
internalStoreRef.current = createViewStore(initialState);
|
|
91
|
+
lastViewRef.current = view;
|
|
92
|
+
}
|
|
93
|
+
store = internalStoreRef.current;
|
|
83
94
|
}
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
95
|
+
// Build ActionProvider handlers from actions mapping
|
|
96
|
+
// Each handler calls actor.send() with the configured event type + params
|
|
97
|
+
const handlers = Object.fromEntries(Object.entries(actions).map(([actionName, eventType]) => [
|
|
98
|
+
actionName,
|
|
99
|
+
async (params = {}) => actor.send({ type: eventType, ...params }),
|
|
100
|
+
]));
|
|
101
|
+
return (_jsx(ActorContext.Provider, { value: actor, children: _jsx(PlayErrorBoundary, { fallback: fallback, children: _jsx(StateProvider, { store: store, children: _jsx(ActionProvider, { handlers: handlers, children: _jsx(VisibilityProvider, { children: _jsx(Renderer, { spec: view.spec ?? null, registry: registry }) }) }) }) }) }));
|
|
87
102
|
};
|
|
88
103
|
//# sourceMappingURL=PlayRenderer.js.map
|
package/dist/PlayRenderer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlayRenderer.js","sourceRoot":"","sources":["../src/PlayRenderer.tsx"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"PlayRenderer.js","sourceRoot":"","sources":["../src/PlayRenderer.tsx"],"names":[],"mappings":";AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAEjG,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAG3D,OAAO,EAAE,YAAY,EAAkB,MAAM,eAAe,CAAC;AAE7D;;;GAGG;AACH,SAAS,eAAe,CAAC,YAAqC;IAC7D,OAAO,qBAAqB,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,CAAC,MAAM,YAAY,GAAgC,CAAC,EACzD,KAAK,EACL,QAAQ,EACR,KAAK,EAAE,aAAa,EACpB,QAAQ,GAAG,IAAI,EACf,OAAO,GAAG,EAAE,GACZ,EAAE,EAAE;IACJ,mEAAmE;IACnE,qEAAqE;IACrE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAsB,GAAG,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC;IAErF,+DAA+D;IAC/D,qFAAqF;IACrF,0CAA0C;IAC1C,MAAM,gBAAgB,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAC;IAEtD,8BAA8B;IAC9B,eAAe,CAAC,GAAG,EAAE;QACpB,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC;QAC5C,OAAO,CAAC,WAAW,CAAC,CAAC;IACtB,CAAC,CAAC,CAAC;IAEH,6CAA6C;IAC7C,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,OAAO,4BAAG,QAAQ,GAAI,CAAC;IACxB,CAAC;IAED,8CAA8C;IAC9C,+DAA+D;IAC/D,0EAA0E;IAC1E,IAAI,KAAiB,CAAC;IACtB,IAAI,aAAa,EAAE,CAAC;QACnB,KAAK,GAAG,aAAa,CAAC;IACvB,CAAC;SAAM,CAAC;QACP,6DAA6D;QAC7D,mEAAmE;QACnE,IAAI,gBAAgB,CAAC,OAAO,KAAK,IAAI,IAAI,WAAW,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YACvE,MAAM,YAAY,GAAI,IAAI,CAAC,IAAI,EAAE,KAAiC,IAAI,EAAE,CAAC;YACzE,gBAAgB,CAAC,OAAO,GAAG,eAAe,CAAC,YAAY,CAAC,CAAC;YACzD,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,CAAC;QACD,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC;IAClC,CAAC;IAED,qDAAqD;IACrD,0EAA0E;IAC1E,MAAM,QAAQ,GAAkC,MAAM,CAAC,WAAW,CACjE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,EAAE,CAAC;QACxD,UAAU;QACV,KAAK,EAAE,SAAkC,EAAE,EAAE,EAAE,CAC9C,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAAC;KAC3C,CAAC,CACF,CAAC;IAEF,OAAO,CACN,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAkB,YAC/C,KAAC,iBAAiB,IAAC,QAAQ,EAAE,QAAQ,YACpC,KAAC,aAAa,IAAC,KAAK,EAAE,KAAK,YAC1B,KAAC,cAAc,IAAC,QAAQ,EAAE,QAAQ,YACjC,KAAC,kBAAkB,cAClB,KAAC,QAAQ,IAAC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,QAAQ,EAAE,QAAQ,GAAI,GACrC,GACL,GACF,GACG,GACG,CACxB,CAAC;AACH,CAAC,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PlayError } from "@xmachines/play";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by `PlayErrorBoundary.componentDidCatch()` when a React component inside
|
|
4
|
+
* the boundary throws during rendering.
|
|
5
|
+
*
|
|
6
|
+
* The original render error is set as `cause`. The `onError` prop callback fires
|
|
7
|
+
* before this error is thrown, allowing observability tools to record the incident.
|
|
8
|
+
*
|
|
9
|
+
* **Error code:** `PLAY_RENDERER_RENDER_ERROR`
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { RendererError } from "@xmachines/play-react/errors";
|
|
14
|
+
*
|
|
15
|
+
* // In a parent error boundary or global handler:
|
|
16
|
+
* if (err instanceof RendererError) {
|
|
17
|
+
* // err.cause is the original component error
|
|
18
|
+
* reportRenderError(err.cause as Error);
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class RendererError extends PlayError {
|
|
23
|
+
constructor(message: string, options?: ErrorOptions);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,aAAc,SAAQ,SAAS;gBAC/B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAInD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PlayError } from "@xmachines/play";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by `PlayErrorBoundary.componentDidCatch()` when a React component inside
|
|
4
|
+
* the boundary throws during rendering.
|
|
5
|
+
*
|
|
6
|
+
* The original render error is set as `cause`. The `onError` prop callback fires
|
|
7
|
+
* before this error is thrown, allowing observability tools to record the incident.
|
|
8
|
+
*
|
|
9
|
+
* **Error code:** `PLAY_RENDERER_RENDER_ERROR`
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { RendererError } from "@xmachines/play-react/errors";
|
|
14
|
+
*
|
|
15
|
+
* // In a parent error boundary or global handler:
|
|
16
|
+
* if (err instanceof RendererError) {
|
|
17
|
+
* // err.cause is the original component error
|
|
18
|
+
* reportRenderError(err.cause as Error);
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export class RendererError extends PlayError {
|
|
23
|
+
constructor(message, options) {
|
|
24
|
+
super("PlayRenderer", "PLAY_RENDERER_RENDER_ERROR", message, options);
|
|
25
|
+
this.name = "RendererError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,aAAc,SAAQ,SAAS;IAC3C,YAAY,OAAe,EAAE,OAAsB;QAClD,KAAK,CAAC,cAAc,EAAE,4BAA4B,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QACtE,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC7B,CAAC;CACD"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,19 +2,27 @@
|
|
|
2
2
|
* @xmachines/play-react - React renderer for XMachines Play architecture
|
|
3
3
|
*
|
|
4
4
|
* Provides a thin React rendering layer that passively observes actor signals
|
|
5
|
-
* and renders UI components
|
|
5
|
+
* and renders UI components via @json-render/react. This package enables
|
|
6
6
|
* framework-swappable architecture where React is just a rendering target
|
|
7
7
|
* that subscribes to signal changes.
|
|
8
8
|
*
|
|
9
9
|
* **Key principle:** React state is NEVER used for business logic—only for
|
|
10
10
|
* triggering React's render cycle. Signals are the source of truth.
|
|
11
11
|
*
|
|
12
|
+
* Re-exports `defineRegistry`, `useStateBinding`, `ComponentFn`, and
|
|
13
|
+
* `ComponentContext` from `@json-render/react` so consumers import everything
|
|
14
|
+
* from `@xmachines/play-react` rather than `@json-render/react` directly.
|
|
15
|
+
*
|
|
12
16
|
* @packageDocumentation
|
|
13
17
|
* @module @xmachines/play-react
|
|
14
18
|
*/
|
|
15
19
|
export { PlayRenderer } from "./PlayRenderer.js";
|
|
16
20
|
export { useSignalEffect } from "./useSignalEffect.js";
|
|
17
21
|
export { PlayErrorBoundary } from "./PlayErrorBoundary.js";
|
|
22
|
+
export { useActor } from "./useActor.js";
|
|
23
|
+
export { defineRegistry, useStateBinding } from "@json-render/react";
|
|
24
|
+
export type { ComponentFn, ComponentContext } from "@json-render/react";
|
|
18
25
|
export type { PlayRendererProps } from "./types.js";
|
|
19
26
|
export type { PlayErrorBoundaryProps, PlayErrorBoundaryState } from "./PlayErrorBoundary.js";
|
|
27
|
+
export type { PlayActor } from "./useActor.js";
|
|
20
28
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAC7F,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* @xmachines/play-react - React renderer for XMachines Play architecture
|
|
3
3
|
*
|
|
4
4
|
* Provides a thin React rendering layer that passively observes actor signals
|
|
5
|
-
* and renders UI components
|
|
5
|
+
* and renders UI components via @json-render/react. This package enables
|
|
6
6
|
* framework-swappable architecture where React is just a rendering target
|
|
7
7
|
* that subscribes to signal changes.
|
|
8
8
|
*
|
|
9
9
|
* **Key principle:** React state is NEVER used for business logic—only for
|
|
10
10
|
* triggering React's render cycle. Signals are the source of truth.
|
|
11
11
|
*
|
|
12
|
+
* Re-exports `defineRegistry`, `useStateBinding`, `ComponentFn`, and
|
|
13
|
+
* `ComponentContext` from `@json-render/react` so consumers import everything
|
|
14
|
+
* from `@xmachines/play-react` rather than `@json-render/react` directly.
|
|
15
|
+
*
|
|
12
16
|
* @packageDocumentation
|
|
13
17
|
* @module @xmachines/play-react
|
|
14
18
|
*/
|
|
@@ -16,4 +20,8 @@
|
|
|
16
20
|
export { PlayRenderer } from "./PlayRenderer.js";
|
|
17
21
|
export { useSignalEffect } from "./useSignalEffect.js";
|
|
18
22
|
export { PlayErrorBoundary } from "./PlayErrorBoundary.js";
|
|
23
|
+
export { useActor } from "./useActor.js";
|
|
24
|
+
// Re-export from @json-render/react so consumers import everything from @xmachines/play-react.
|
|
25
|
+
// React's useContext works anywhere in the call tree — no wrapper needed.
|
|
26
|
+
export { defineRegistry, useStateBinding } from "@json-render/react";
|
|
19
27
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,eAAe;AACf,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,+FAA+F;AAC/F,0EAA0E;AAC1E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -3,22 +3,52 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
|
+
import type { ComponentRegistry } from "@json-render/react";
|
|
7
|
+
import type { StateStore } from "@json-render/core";
|
|
6
8
|
import type { AbstractActor, Viewable } from "@xmachines/play-actor";
|
|
7
9
|
import type React from "react";
|
|
8
|
-
import type { AnyActorLogic } from "xstate";
|
|
10
|
+
import type { AnyActorLogic, EventFromLogic } from "xstate";
|
|
9
11
|
/**
|
|
10
12
|
* Props for PlayRenderer component
|
|
11
13
|
*
|
|
14
|
+
* @typeParam TLogic - The XState actor logic type. Defaults to `AnyActorLogic` for
|
|
15
|
+
* non-generic usage. When a specific machine type is provided, the `actions` values
|
|
16
|
+
* are constrained to the event type strings that the machine actually accepts.
|
|
17
|
+
*
|
|
12
18
|
* @property actor - Actor instance with currentView signal (requires Viewable capability)
|
|
13
|
-
* @property
|
|
19
|
+
* @property registry - ComponentRegistry from defineRegistry() in @json-render/react
|
|
20
|
+
* @property store - Optional external StateStore (e.g. from @json-render/xstate). When
|
|
21
|
+
* provided, PlayRenderer operates in controlled mode — spec.state is ignored and the
|
|
22
|
+
* store is the single source of truth for UI state. When omitted, a fresh
|
|
23
|
+
* @xstate/store atom is created internally per view transition, seeded from spec.state.
|
|
14
24
|
* @property fallback - Optional component shown when currentView is null
|
|
25
|
+
* @property actions - Maps json-render action names to XState event type strings.
|
|
26
|
+
* Values are constrained to `EventFromLogic<TLogic>["type"]` — passing a non-existent
|
|
27
|
+
* event type string is a compile error when TLogic is specified.
|
|
15
28
|
*/
|
|
16
|
-
export interface PlayRendererProps {
|
|
29
|
+
export interface PlayRendererProps<TLogic extends AnyActorLogic = AnyActorLogic> {
|
|
17
30
|
/** Actor instance with currentView signal (requires Viewable capability) */
|
|
18
|
-
actor: AbstractActor<
|
|
19
|
-
/**
|
|
20
|
-
|
|
31
|
+
actor: AbstractActor<TLogic> & Viewable;
|
|
32
|
+
/** ComponentRegistry from defineRegistry() in @json-render/react */
|
|
33
|
+
registry: ComponentRegistry;
|
|
34
|
+
/**
|
|
35
|
+
* Optional external StateStore (e.g. from `xstateStoreStateStore` in @json-render/xstate).
|
|
36
|
+
* When provided, PlayRenderer operates in controlled mode — spec.state is ignored and
|
|
37
|
+
* this store is the single source of truth for UI state (form values, etc.).
|
|
38
|
+
* When omitted, a fresh @xstate/store atom is created internally per view transition,
|
|
39
|
+
* seeded from spec.state. The atom resets automatically when the actor transitions to a
|
|
40
|
+
* new view, mirroring the actor's currentView lifecycle.
|
|
41
|
+
*/
|
|
42
|
+
store?: StateStore;
|
|
21
43
|
/** Optional component shown when currentView is null */
|
|
22
44
|
fallback?: React.ReactNode;
|
|
45
|
+
/**
|
|
46
|
+
* Maps json-render action names to XState event type strings.
|
|
47
|
+
* Values are constrained to valid event types for TLogic — wrong event type strings
|
|
48
|
+
* are caught at compile time when TLogic is specified.
|
|
49
|
+
*/
|
|
50
|
+
actions?: {
|
|
51
|
+
[actionName: string]: EventFromLogic<TLogic>["type"];
|
|
52
|
+
};
|
|
23
53
|
}
|
|
24
54
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAE5D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,iBAAiB,CAAC,MAAM,SAAS,aAAa,GAAG,aAAa;IAC9E,4EAA4E;IAC5E,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC;IAExC,oEAAoE;IACpE,QAAQ,EAAE,iBAAiB,CAAC;IAE5B;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IAEnB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE3B;;;;OAIG;IACH,OAAO,CAAC,EAAE;QAAE,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;KAAE,CAAC;CACnE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useActor — React hook for accessing the raw actor inside a PlayRenderer tree.
|
|
3
|
+
*
|
|
4
|
+
* Components rendered inside PlayRenderer can call useActor() to get direct
|
|
5
|
+
* access to the actor instance without prop drilling.
|
|
6
|
+
*
|
|
7
|
+
* @throws {Error} If called outside a PlayRenderer tree
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { useActor } from "@xmachines/play-react";
|
|
12
|
+
*
|
|
13
|
+
* function MyComponent() {
|
|
14
|
+
* const actor = useActor();
|
|
15
|
+
* return <button onClick={() => actor.send({ type: "SUBMIT" })}>Submit</button>;
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
import type { AbstractActor } from "@xmachines/play-actor";
|
|
22
|
+
import type { AnyActorLogic } from "xstate";
|
|
23
|
+
export type PlayActor = AbstractActor<AnyActorLogic>;
|
|
24
|
+
export declare const ActorContext: import("react").Context<PlayActor | null>;
|
|
25
|
+
export declare function useActor(): PlayActor;
|
|
26
|
+
//# sourceMappingURL=useActor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useActor.d.ts","sourceRoot":"","sources":["../src/useActor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE5C,MAAM,MAAM,SAAS,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;AAErD,eAAO,MAAM,YAAY,2CAAwC,CAAC;AAElE,wBAAgB,QAAQ,IAAI,SAAS,CAIpC"}
|
package/dist/useActor.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useActor — React hook for accessing the raw actor inside a PlayRenderer tree.
|
|
3
|
+
*
|
|
4
|
+
* Components rendered inside PlayRenderer can call useActor() to get direct
|
|
5
|
+
* access to the actor instance without prop drilling.
|
|
6
|
+
*
|
|
7
|
+
* @throws {Error} If called outside a PlayRenderer tree
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { useActor } from "@xmachines/play-react";
|
|
12
|
+
*
|
|
13
|
+
* function MyComponent() {
|
|
14
|
+
* const actor = useActor();
|
|
15
|
+
* return <button onClick={() => actor.send({ type: "SUBMIT" })}>Submit</button>;
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
import { createContext, useContext } from "react";
|
|
22
|
+
export const ActorContext = createContext(null);
|
|
23
|
+
export function useActor() {
|
|
24
|
+
const actor = useContext(ActorContext);
|
|
25
|
+
if (!actor)
|
|
26
|
+
throw new Error("useActor() must be called inside <PlayRenderer>");
|
|
27
|
+
return actor;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=useActor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useActor.js","sourceRoot":"","sources":["../src/useActor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAMlD,MAAM,CAAC,MAAM,YAAY,GAAG,aAAa,CAAmB,IAAI,CAAC,CAAC;AAElE,MAAM,UAAU,QAAQ;IACvB,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IAC/E,OAAO,KAAK,CAAC;AACd,CAAC"}
|
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.17",
|
|
4
4
|
"description": "React renderer for XMachines Play architecture with signal-driven rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"actor",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"build": "tsc --build",
|
|
38
38
|
"clean": "rm -rf dist *.tsbuildinfo node_modules/.vite node_modules/.vite-temp",
|
|
39
39
|
"typecheck": "tsc --noEmit",
|
|
40
|
+
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
|
40
41
|
"typecheck:browser": "tsc --noEmit -p tsconfig.browser.json",
|
|
41
42
|
"test": "vitest",
|
|
42
43
|
"test:vitest": "vitest run",
|
|
@@ -44,23 +45,31 @@
|
|
|
44
45
|
"prepublishOnly": "npm run build"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
|
-
"@xmachines/play-actor": "1.0.0-beta.
|
|
48
|
-
"@xmachines/play-
|
|
49
|
-
"@xmachines/play-signals": "1.0.0-beta.15"
|
|
48
|
+
"@xmachines/play-actor": "1.0.0-beta.17",
|
|
49
|
+
"@xmachines/play-signals": "1.0.0-beta.17"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@json-render/core": "^0.16.0",
|
|
53
|
+
"@json-render/react": "^0.16.0",
|
|
54
|
+
"@json-render/xstate": "^0.16.0",
|
|
52
55
|
"@testing-library/jest-dom": "^6.9.1",
|
|
53
56
|
"@testing-library/react": "^16.3.2",
|
|
54
57
|
"@types/node": "^25.5.0",
|
|
55
58
|
"@types/react": "^19.2.14",
|
|
56
59
|
"@types/react-dom": "^19.2.3",
|
|
57
|
-
"@xmachines/shared": "1.0.0-beta.
|
|
60
|
+
"@xmachines/shared": "1.0.0-beta.17",
|
|
61
|
+
"@xstate/store": ">=3.17.0",
|
|
58
62
|
"jsdom": "^29.0.1",
|
|
59
63
|
"react": "^19.2.4",
|
|
60
64
|
"react-dom": "^19.2.4",
|
|
61
|
-
"typescript": "^5.9.3"
|
|
65
|
+
"typescript": "^5.9.3 || ^6.0.2",
|
|
66
|
+
"vitest": "^4.1.2"
|
|
62
67
|
},
|
|
63
68
|
"peerDependencies": {
|
|
69
|
+
"@json-render/core": "^0.16.0",
|
|
70
|
+
"@json-render/react": "^0.16.0",
|
|
71
|
+
"@json-render/xstate": "^0.16.0",
|
|
72
|
+
"@xstate/store": ">=3.17.0",
|
|
64
73
|
"react": "^18.0.0 || ^19.0.0",
|
|
65
74
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
66
75
|
}
|