@typeonce/effect-machine 0.27.1 → 0.29.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/README.md +41 -4
- package/dist/internal/machine/atom.d.ts +9 -0
- package/dist/internal/machine/atom.d.ts.map +1 -1
- package/dist/internal/machine/atom.js +80 -7
- package/dist/internal/machine/atom.js.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.d.ts +144 -17
- package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -1
- package/dist/unstable/reactivity/AtomMachine.js +77 -17
- package/dist/unstable/reactivity/AtomMachine.js.map +1 -1
- package/docs/effect-atom-react.md +218 -155
- package/docs/machine-review.md +41 -23
- package/package.json +1 -1
- package/src/internal/machine/atom.ts +185 -19
- package/src/unstable/reactivity/AtomMachine.ts +479 -70
|
@@ -1,230 +1,293 @@
|
|
|
1
|
-
# Effect Atom and React
|
|
1
|
+
# Effect Atom and React
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
React code should own one stable machine atom, pass it through props or
|
|
4
|
+
Context, and subscribe in the descendants that render machine state. Keep the
|
|
5
|
+
machine definition free of React dependencies.
|
|
6
|
+
|
|
7
|
+
Read the [Effect Machine agent guide](./agent-guide.md) for statechart
|
|
8
|
+
modeling, transitions, services, and testing.
|
|
7
9
|
|
|
8
10
|
## Recommended folder structure
|
|
9
11
|
|
|
10
12
|
```text
|
|
11
13
|
src/
|
|
12
|
-
├── context/
|
|
13
|
-
│
|
|
14
|
-
│ └── process-context.tsx
|
|
14
|
+
├── context/
|
|
15
|
+
│ └── auth-machine-context.tsx # React ownership and distribution
|
|
15
16
|
├── lib/
|
|
16
|
-
│ ├── atom-runtime.ts
|
|
17
|
-
│ └── services/
|
|
18
|
-
│ └── query-processor.ts
|
|
17
|
+
│ ├── atom-runtime.ts # Shared bound AtomMachine runtime
|
|
18
|
+
│ └── services/
|
|
19
19
|
└── machines/
|
|
20
|
-
|
|
21
|
-
│ ├── machine.ts # Machine implementation
|
|
22
|
-
│ └── atom.ts # Focused atoms for React
|
|
23
|
-
├── process/
|
|
24
|
-
│ ├── machine.ts
|
|
25
|
-
│ └── atom.ts
|
|
26
|
-
└── dialog/
|
|
27
|
-
├── machine.ts
|
|
28
|
-
└── atom.ts
|
|
20
|
+
└── auth-machine.ts # States, events, and behavior
|
|
29
21
|
```
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
`machine.ts` owns the workflow. A Context module only creates and distributes
|
|
24
|
+
the machine atom. State-slot components decide which state paths they render.
|
|
32
25
|
|
|
33
|
-
-
|
|
34
|
-
Effect service requirements. It has no React dependency.
|
|
35
|
-
- `atom.ts` adapts that machine to the shared bound AtomMachine runtime and
|
|
36
|
-
exports the focused atoms React needs.
|
|
37
|
-
- `lib/services/` contains reusable business services used by machines.
|
|
38
|
-
- `context/` is optional. It only distributes an already-created machine scope
|
|
39
|
-
through a React subtree.
|
|
40
|
-
- `lib/atom-runtime.ts` binds AtomMachine once to the application's Effect
|
|
41
|
-
service layer:
|
|
26
|
+
Bind service-backed machines once at the application runtime:
|
|
42
27
|
|
|
43
28
|
```ts
|
|
44
29
|
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
45
30
|
import { Atom } from "effect/unstable/reactivity"
|
|
46
|
-
import {
|
|
31
|
+
import { AppLayer } from "./app-layer"
|
|
47
32
|
|
|
48
|
-
const atomRuntime = Atom.runtime(
|
|
33
|
+
const atomRuntime = Atom.runtime(AppLayer)
|
|
49
34
|
|
|
50
|
-
export const
|
|
35
|
+
export const MachineAtoms = AtomMachine.bind(atomRuntime)
|
|
51
36
|
```
|
|
52
37
|
|
|
53
|
-
|
|
38
|
+
Service-free machines can use `AtomMachine.make` directly.
|
|
54
39
|
|
|
55
|
-
##
|
|
40
|
+
## Own a machine in one React subtree
|
|
56
41
|
|
|
57
|
-
Use
|
|
58
|
-
|
|
42
|
+
Use `useMachineAtom` when a provider, route, dialog, or other React subtree
|
|
43
|
+
owns one machine instance:
|
|
59
44
|
|
|
60
|
-
```
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
45
|
+
```tsx
|
|
46
|
+
import { useMachineAtom } from "@typeonce/effect-machine-react"
|
|
47
|
+
import { createContext, type ReactNode, useContext } from "react"
|
|
48
|
+
import { AuthMachine, type AuthMachineInput } from "../machines/auth-machine"
|
|
49
|
+
import { MachineAtoms } from "../lib/atom-runtime"
|
|
64
50
|
|
|
65
|
-
|
|
51
|
+
const makeAuthMachine = (input: AuthMachineInput) => MachineAtoms.make(AuthMachine, input)
|
|
52
|
+
type AuthMachineAtom = ReturnType<typeof makeAuthMachine>
|
|
66
53
|
|
|
67
|
-
|
|
68
|
-
counterMachineAtom,
|
|
69
|
-
"counter"
|
|
70
|
-
)
|
|
71
|
-
```
|
|
54
|
+
const AuthMachineContext = createContext<AuthMachineAtom | null>(null)
|
|
72
55
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
56
|
+
export function AuthMachineProvider({
|
|
57
|
+
children,
|
|
58
|
+
input
|
|
59
|
+
}: {
|
|
60
|
+
readonly children: ReactNode
|
|
61
|
+
readonly input: AuthMachineInput
|
|
62
|
+
}) {
|
|
63
|
+
const machine = useMachineAtom(() => makeAuthMachine(input))
|
|
76
64
|
|
|
77
|
-
|
|
65
|
+
return (
|
|
66
|
+
<AuthMachineContext.Provider value={machine}>
|
|
67
|
+
{children}
|
|
68
|
+
</AuthMachineContext.Provider>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
export function useAuthMachine(): AuthMachineAtom {
|
|
73
|
+
const machine = useContext(AuthMachineContext)
|
|
74
|
+
if (machine === null) {
|
|
75
|
+
throw new Error("useAuthMachine must be used inside AuthMachineProvider")
|
|
76
|
+
}
|
|
77
|
+
return machine
|
|
78
|
+
}
|
|
79
|
+
```
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
import { processMachine } from "./machine"
|
|
81
|
+
The provider strongly owns the complete `MachineAtom`. The hook mounts
|
|
82
|
+
`machine.ref` after React commits the owner, but it does not read `state`,
|
|
83
|
+
`snapshot`, or `result`. Machine updates therefore do not rerender the
|
|
84
|
+
provider.
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
The factory captures startup input once. A later `input` prop change does not
|
|
87
|
+
replace the running workflow. Send an event when the change belongs to that
|
|
88
|
+
workflow. Change the provider's React key when React should own a new machine:
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
})
|
|
90
|
+
```tsx
|
|
91
|
+
<AuthMachineProvider key={attemptId} input={input}>
|
|
92
|
+
<AuthCard />
|
|
93
|
+
</AuthMachineProvider>
|
|
97
94
|
```
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`ReturnType<typeof processFamily>`. The provider resolves the query once instead
|
|
102
|
-
of drilling it through every component.
|
|
103
|
-
|
|
104
|
-
Changing `query` selects another family member and therefore another machine.
|
|
105
|
-
If a changing value should update the current workflow, model it as an event.
|
|
96
|
+
Put the owner above a Suspense boundary. React can then retain the same machine
|
|
97
|
+
while a state-reading descendant suspends.
|
|
106
98
|
|
|
107
|
-
##
|
|
99
|
+
## Render state-owned data
|
|
108
100
|
|
|
109
|
-
|
|
101
|
+
Subscribe in the smallest component that renders a state path:
|
|
110
102
|
|
|
111
|
-
```
|
|
112
|
-
import {
|
|
103
|
+
```tsx
|
|
104
|
+
import { useAtomSuspense } from "@effect/atom-react"
|
|
113
105
|
import { AtomMachine } from "@typeonce/effect-machine/reactivity"
|
|
114
|
-
import {
|
|
106
|
+
import { Option } from "effect"
|
|
107
|
+
|
|
108
|
+
function EditingFields() {
|
|
109
|
+
const machine = useAuthMachine()
|
|
110
|
+
const editing = useAtomSuspense(
|
|
111
|
+
AtomMachine.selectSnapshot(machine, "Editing")
|
|
112
|
+
).value
|
|
113
|
+
|
|
114
|
+
return Option.match(editing, {
|
|
115
|
+
onNone: () => null,
|
|
116
|
+
onSome: ({ value }) => <EmailField email={value.email} />
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
```
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
`AtomMachine.select` returns the selected state value.
|
|
122
|
+
`AtomMachine.selectSnapshot` also retains the selected state's child topology.
|
|
123
|
+
Both return `Option.none()` while the path is inactive. Do not replace that
|
|
124
|
+
absence with an empty string, `null`, or a global boolean.
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
Repeated calls with the same machine and path return the same atom, so path
|
|
127
|
+
selection is safe during render without `useMemo`. Equal selected values do not
|
|
128
|
+
notify the component.
|
|
129
|
+
|
|
130
|
+
Nested paths keep the same ownership:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
function PasswordField() {
|
|
134
|
+
const machine = useAuthMachine()
|
|
135
|
+
const password = useAtomSuspense(
|
|
136
|
+
AtomMachine.select(machine, "Editing.Password")
|
|
137
|
+
).value
|
|
138
|
+
|
|
139
|
+
return Option.match(password, {
|
|
140
|
+
onNone: () => null,
|
|
141
|
+
onSome: ({ password }) => <input type="password" value={password} />
|
|
142
|
+
})
|
|
125
143
|
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Place independent subscriptions in independent descendants:
|
|
126
147
|
|
|
127
|
-
|
|
148
|
+
```tsx
|
|
149
|
+
function AuthCard() {
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<EditingFields />
|
|
153
|
+
<VerificationFields />
|
|
154
|
+
<FailureMessage />
|
|
155
|
+
<SubmitButton />
|
|
156
|
+
</>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
128
159
|
```
|
|
129
160
|
|
|
130
|
-
|
|
161
|
+
Atom granularity cannot isolate hooks that all live in `AuthCard`. Any selected
|
|
162
|
+
change rerenders the component that called the hook.
|
|
131
163
|
|
|
132
|
-
|
|
164
|
+
## Send without subscribing
|
|
165
|
+
|
|
166
|
+
Use the writable atom directly:
|
|
133
167
|
|
|
134
168
|
```tsx
|
|
135
|
-
|
|
169
|
+
import { useAtomSet } from "@effect/atom-react"
|
|
136
170
|
|
|
137
|
-
|
|
138
|
-
const
|
|
171
|
+
function SubmitButton() {
|
|
172
|
+
const machine = useAuthMachine()
|
|
173
|
+
const send = useAtomSet(machine.send)
|
|
139
174
|
|
|
140
175
|
return (
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
</
|
|
176
|
+
<button onClick={() => send({ _tag: "Submitted" })}>
|
|
177
|
+
Continue
|
|
178
|
+
</button>
|
|
144
179
|
)
|
|
145
180
|
}
|
|
146
181
|
```
|
|
147
182
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`DialogScope` through props when Context is unnecessary. Do not add a wrapper
|
|
151
|
-
component whose only job is forwarding the scope.
|
|
183
|
+
`useAtomSet` mounts the writable atom and does not subscribe the component to
|
|
184
|
+
its value.
|
|
152
185
|
|
|
153
|
-
|
|
186
|
+
## Whole-result and custom selections
|
|
154
187
|
|
|
155
|
-
|
|
156
|
-
|
|
188
|
+
Reading the full result is correct when a component renders the complete
|
|
189
|
+
machine state:
|
|
157
190
|
|
|
158
|
-
```
|
|
159
|
-
|
|
191
|
+
```tsx
|
|
192
|
+
function AuthScreen() {
|
|
193
|
+
const machine = useAuthMachine()
|
|
194
|
+
const state = useAtomSuspense(machine.result).value
|
|
195
|
+
|
|
196
|
+
return AuthStates.match(state, {
|
|
197
|
+
Editing: (editing) => <EditingScreen state={editing} />,
|
|
198
|
+
Verification: (verification) => <VerificationScreen state={verification} />,
|
|
199
|
+
Failed: (failed) => <FailureScreen state={failed} />
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
```
|
|
160
203
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
openStateAtom: scope.openStateAtom.pipe(
|
|
169
|
-
Atom.withLabel(`dialog:${dialogId}:openState-source`)
|
|
170
|
-
),
|
|
171
|
-
sendAtom: scope.sendAtom.pipe(
|
|
172
|
-
Atom.withLabel(`dialog:${dialogId}:send-source`)
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
})
|
|
204
|
+
That component rerenders for every result change. Current
|
|
205
|
+
`@effect/atom-react` does not select from the successful value in
|
|
206
|
+
`useAtomSuspense`. Until it does, use typed path selectors for state-owned UI,
|
|
207
|
+
or declare a custom derived atom once in a strongly owned scope. Do not create
|
|
208
|
+
a fresh derived atom on every render.
|
|
209
|
+
|
|
210
|
+
## Share a keyed machine outside one React owner
|
|
176
211
|
|
|
177
|
-
|
|
178
|
-
|
|
212
|
+
`AtomMachine.family` is for registry-owned machines that unrelated consumers
|
|
213
|
+
find by startup input. It is not the default for one React-owned workflow.
|
|
179
214
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
215
|
+
```ts
|
|
216
|
+
export const processAtoms = MachineAtoms.family(ProcessMachine, {
|
|
217
|
+
atoms: {
|
|
218
|
+
details: AtomMachine.select("Processing"),
|
|
219
|
+
ready: AtomMachine.matches("Ready"),
|
|
220
|
+
send: (machine) => machine.send
|
|
221
|
+
}
|
|
184
222
|
})
|
|
223
|
+
```
|
|
185
224
|
|
|
186
|
-
|
|
187
|
-
const scope = dialogScopeFamily(dialogId)
|
|
225
|
+
Consumers use the input as the shared identity key:
|
|
188
226
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
).pipe(Atom.withLabel(`dialog:${dialogId}:openState`))
|
|
193
|
-
})
|
|
227
|
+
```tsx
|
|
228
|
+
const details = useAtomSuspense(processAtoms.details(input)).value
|
|
229
|
+
const send = useAtomSet(processAtoms.send(input))
|
|
194
230
|
```
|
|
195
231
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
the inferred dialog events.
|
|
232
|
+
Each public projection retains its private machine owner. Keeping only
|
|
233
|
+
`details(input)` or `send(input)` is safe. Do not return a weakly held composite
|
|
234
|
+
scope and retain only one field from it.
|
|
200
235
|
|
|
201
|
-
|
|
202
|
-
|
|
236
|
+
Family keys use Effect `Equal` and `Hash` semantics. Keep them immutable. If a
|
|
237
|
+
changing value should update one running workflow, model it as an event instead
|
|
238
|
+
of changing the family key.
|
|
203
239
|
|
|
204
|
-
##
|
|
240
|
+
## Module-owned machines
|
|
205
241
|
|
|
206
|
-
|
|
207
|
-
machines:
|
|
242
|
+
A no-input machine may intentionally have one module-owned identity:
|
|
208
243
|
|
|
209
244
|
```ts
|
|
210
|
-
const
|
|
245
|
+
export const CounterMachineAtom = MachineAtoms.make(CounterMachine)
|
|
246
|
+
export const CounterStateAtom = AtomMachine.select(CounterMachineAtom, "Count")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Every consumer using the same `AtomRegistry` reaches the same running machine.
|
|
250
|
+
Different registries still run independent instances.
|
|
251
|
+
|
|
252
|
+
## Child machines
|
|
211
253
|
|
|
212
|
-
|
|
254
|
+
Direct child selectors follow the active child and preserve inactivity:
|
|
213
255
|
|
|
214
|
-
|
|
215
|
-
|
|
256
|
+
```tsx
|
|
257
|
+
const editor = machine.child(Editor)
|
|
258
|
+
const editing = useAtomSuspense(
|
|
259
|
+
AtomMachine.selectSnapshotChild(editor, "Editing")
|
|
260
|
+
).value
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
An inactive child or path returns `Option.none()`. Re-entry follows the
|
|
264
|
+
replacement child instance.
|
|
265
|
+
|
|
266
|
+
Use `AtomMachine.familyChild` when a parent owns a runtime-sized set of keyed
|
|
267
|
+
children:
|
|
216
268
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
269
|
+
```ts
|
|
270
|
+
const Plant = Machine.childFamily(PlantMachine)
|
|
271
|
+
|
|
272
|
+
export const plantAtoms = AtomMachine.familyChild(CentralMachineAtom, {
|
|
273
|
+
child: (plantId: string) => Plant(plantId),
|
|
274
|
+
atoms: {
|
|
275
|
+
broken: AtomMachine.matchesChild("Broken"),
|
|
276
|
+
state: (plant) => plant.state,
|
|
277
|
+
send: (plant) => plant.send
|
|
222
278
|
}
|
|
223
279
|
})
|
|
224
280
|
```
|
|
225
281
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
282
|
+
## Registry and rendering semantics
|
|
283
|
+
|
|
284
|
+
A `MachineAtom` identifies one machine per `AtomRegistry`. Passing the same
|
|
285
|
+
machine atom through two registry providers creates two independent runtimes.
|
|
286
|
+
Unmounting a React owner releases its mount. The registry stops the machine
|
|
287
|
+
after its final subscription and configured idle retention expire.
|
|
288
|
+
`registry.dispose()` stops it immediately.
|
|
289
|
+
|
|
290
|
+
`useMachineAtom` does not start a machine during server rendering because
|
|
291
|
+
React effects do not run on the server. Reading a machine atom during server
|
|
292
|
+
render follows `@effect/atom-react` server-read behavior, so choose an explicit
|
|
293
|
+
client boundary when server startup would be undesirable.
|
package/docs/machine-review.md
CHANGED
|
@@ -65,27 +65,29 @@ const handlers = {
|
|
|
65
65
|
Review check: search for `.resolve(...)` callbacks that only return an empty
|
|
66
66
|
`target.from()` and remove the callback.
|
|
67
67
|
|
|
68
|
-
##
|
|
68
|
+
## Choose React ownership or keyed family lookup
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
separate components perform the lookup.
|
|
70
|
+
Use `useMachineAtom` when one React subtree owns the workflow, including a
|
|
71
|
+
machine with startup input:
|
|
73
72
|
|
|
74
73
|
```tsx
|
|
75
|
-
|
|
76
|
-
const scope = useMemo(() => processFamily(processId), [processId])
|
|
77
|
-
|
|
78
|
-
// The family owns identity
|
|
79
|
-
const scope = processFamily(processId)
|
|
74
|
+
const machine = useMachineAtom(() => machineAtoms.make(processMachine, input))
|
|
80
75
|
```
|
|
81
76
|
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
Pass the stable machine through props or Context. Startup input is captured
|
|
78
|
+
once. Send an event to change the running workflow, or change the owner's React
|
|
79
|
+
key to replace it.
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
Use `AtomMachine.family` when unrelated consumers must find one shared machine
|
|
82
|
+
by its startup input. Effect Atom keeps a family value for an equal key while
|
|
83
|
+
that returned value is reachable. Current runtimes may hold family values
|
|
84
|
+
through `WeakRef`. Retaining one field from a composite family value does not
|
|
85
|
+
retain the composite itself:
|
|
88
86
|
|
|
87
|
+
```ts
|
|
88
|
+
// Unsafe when consumers retain only stateAtom or sendAtom
|
|
89
|
+
const processScope = Atom.family((input: ProcessInput) => {
|
|
90
|
+
const machine = machineAtoms.make(processMachine, input)
|
|
89
91
|
return {
|
|
90
92
|
stateAtom: AtomMachine.select(machine, "process"),
|
|
91
93
|
sendAtom: machine.send
|
|
@@ -93,17 +95,33 @@ export const processFamily = Atom.family((processId: string) => {
|
|
|
93
95
|
})
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
`AtomMachine.family` returns direct atom families whose atoms retain the
|
|
99
|
+
private machine bridge:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
export const processAtoms = machineAtoms.family(processMachine, {
|
|
103
|
+
atoms: {
|
|
104
|
+
state: AtomMachine.select("process"),
|
|
105
|
+
send: (machine) => machine.send
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const stateAtom = processAtoms.state(input)
|
|
110
|
+
const sendAtom = processAtoms.send(input)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
No component `useMemo` is needed. The registry retains the public atom while a
|
|
114
|
+
hook subscribes to it, and that atom retains the machine owner. Equal inputs
|
|
115
|
+
use Effect `Equal` and `Hash` semantics and select the same family value.
|
|
98
116
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
subtree, use a lazy `useState(makeScope)` initializer as described in the React
|
|
102
|
-
guide.
|
|
117
|
+
For a no-input machine, use one module-level bridge or `useMachineAtom` in the
|
|
118
|
+
owning React subtree. Do not add an unused family key.
|
|
103
119
|
|
|
104
|
-
Review check: search for `
|
|
105
|
-
`
|
|
106
|
-
|
|
120
|
+
Review check: search for composite `Atom.family` values that own a machine,
|
|
121
|
+
`useMemo` around family lookup, repeated input propagation through one React
|
|
122
|
+
subtree, and component-local calls to `machineAtoms.make` without a stable
|
|
123
|
+
owner. Choose `AtomMachine.family` only when consumers need shared keyed
|
|
124
|
+
lookup.
|
|
107
125
|
|
|
108
126
|
## Justify each `RegistryProvider`
|
|
109
127
|
|