@doeixd/machine 0.0.4 → 0.0.6
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 +952 -13
- package/dist/cjs/development/index.js +691 -0
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +5 -1
- package/dist/esm/development/index.js +698 -0
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +5 -1
- package/dist/types/extract.d.ts +71 -0
- package/dist/types/extract.d.ts.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/multi.d.ts +838 -0
- package/dist/types/multi.d.ts.map +1 -0
- package/dist/types/primitives.d.ts +202 -0
- package/dist/types/primitives.d.ts.map +1 -0
- package/dist/types/runtime-extract.d.ts +53 -0
- package/dist/types/runtime-extract.d.ts.map +1 -0
- package/package.json +6 -2
- package/src/extract.ts +452 -67
- package/src/index.ts +49 -0
- package/src/multi.ts +1145 -0
- package/src/primitives.ts +135 -0
- package/src/react.ts +349 -28
- package/src/runtime-extract.ts +141 -0
- package/src/solid.ts +8 -8
package/src/primitives.ts
CHANGED
|
@@ -21,6 +21,13 @@
|
|
|
21
21
|
*/
|
|
22
22
|
export const META_KEY = Symbol("MachineMeta");
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Runtime metadata symbol.
|
|
26
|
+
* Non-enumerable property key for storing metadata on function objects at runtime.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export const RUNTIME_META = Symbol('__machine_runtime_meta__');
|
|
30
|
+
|
|
24
31
|
/**
|
|
25
32
|
* Helper type representing a Class Constructor.
|
|
26
33
|
* Used to reference target states by their class definition rather than magic strings.
|
|
@@ -87,6 +94,66 @@ export type WithMeta<
|
|
|
87
94
|
M extends TransitionMeta
|
|
88
95
|
> = F & { [META_KEY]: M };
|
|
89
96
|
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// SECTION: RUNTIME METADATA ATTACHMENT
|
|
99
|
+
// =============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Runtime metadata interface (resolved class names as strings)
|
|
103
|
+
*/
|
|
104
|
+
export interface RuntimeTransitionMeta {
|
|
105
|
+
target?: string;
|
|
106
|
+
description?: string;
|
|
107
|
+
guards?: Array<{ name: string; description?: string }>;
|
|
108
|
+
invoke?: {
|
|
109
|
+
src: string;
|
|
110
|
+
onDone: string;
|
|
111
|
+
onError: string;
|
|
112
|
+
description?: string;
|
|
113
|
+
};
|
|
114
|
+
actions?: Array<{ name: string; description?: string }>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Attaches runtime metadata to a function object.
|
|
119
|
+
* Merges with existing metadata if present.
|
|
120
|
+
*
|
|
121
|
+
* @param fn - The function to attach metadata to
|
|
122
|
+
* @param metadata - Partial metadata to merge
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
function attachRuntimeMeta(fn: any, metadata: Partial<RuntimeTransitionMeta>): void {
|
|
126
|
+
// Read existing metadata (may be undefined)
|
|
127
|
+
const existing = fn[RUNTIME_META] || {};
|
|
128
|
+
|
|
129
|
+
// Shallow merge for simple properties
|
|
130
|
+
const merged: any = { ...existing, ...metadata };
|
|
131
|
+
|
|
132
|
+
// Deep merge for array properties
|
|
133
|
+
// Prepend new items to preserve order (outer wraps first in call stack)
|
|
134
|
+
if (metadata.guards && existing.guards) {
|
|
135
|
+
merged.guards = [...metadata.guards, ...existing.guards];
|
|
136
|
+
} else if (metadata.guards) {
|
|
137
|
+
merged.guards = [...metadata.guards];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (metadata.actions && existing.actions) {
|
|
141
|
+
merged.actions = [...metadata.actions, ...existing.actions];
|
|
142
|
+
} else if (metadata.actions) {
|
|
143
|
+
merged.actions = [...metadata.actions];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Replace invoke entirely (not an array, can't merge)
|
|
147
|
+
// Last invoke wins (this matches XState semantics)
|
|
148
|
+
|
|
149
|
+
// Define or redefine the metadata property
|
|
150
|
+
Object.defineProperty(fn, RUNTIME_META, {
|
|
151
|
+
value: merged,
|
|
152
|
+
enumerable: false,
|
|
153
|
+
writable: false,
|
|
154
|
+
configurable: true // CRITICAL: Must be configurable for re-definition
|
|
155
|
+
});
|
|
156
|
+
}
|
|
90
157
|
|
|
91
158
|
// =============================================================================
|
|
92
159
|
// SECTION: ANNOTATION PRIMITIVES (THE DSL)
|
|
@@ -109,6 +176,11 @@ export function transitionTo<
|
|
|
109
176
|
_target: T,
|
|
110
177
|
implementation: F
|
|
111
178
|
): WithMeta<F, { target: T }> {
|
|
179
|
+
// Attach runtime metadata with class name
|
|
180
|
+
attachRuntimeMeta(implementation, {
|
|
181
|
+
target: _target.name || _target.toString()
|
|
182
|
+
});
|
|
183
|
+
|
|
112
184
|
return implementation as any;
|
|
113
185
|
}
|
|
114
186
|
|
|
@@ -127,6 +199,11 @@ export function describe<
|
|
|
127
199
|
_text: string,
|
|
128
200
|
transition: WithMeta<F, M>
|
|
129
201
|
): WithMeta<F, M & { description: string }> {
|
|
202
|
+
// Attach runtime metadata
|
|
203
|
+
attachRuntimeMeta(transition, {
|
|
204
|
+
description: _text
|
|
205
|
+
});
|
|
206
|
+
|
|
130
207
|
return transition as any;
|
|
131
208
|
}
|
|
132
209
|
|
|
@@ -146,6 +223,12 @@ export function guarded<
|
|
|
146
223
|
guard: GuardMeta,
|
|
147
224
|
transition: WithMeta<F, M>
|
|
148
225
|
): WithMeta<F, M & { guards: [typeof guard] }> {
|
|
226
|
+
// Attach runtime metadata
|
|
227
|
+
// Note: guards is an array, will be merged by attachRuntimeMeta
|
|
228
|
+
attachRuntimeMeta(transition, {
|
|
229
|
+
guards: [guard]
|
|
230
|
+
});
|
|
231
|
+
|
|
149
232
|
return transition as any;
|
|
150
233
|
}
|
|
151
234
|
|
|
@@ -168,6 +251,16 @@ export function invoke<
|
|
|
168
251
|
service: { src: string; onDone: D; onError: E; description?: string },
|
|
169
252
|
implementation: F
|
|
170
253
|
): WithMeta<F, { invoke: typeof service }> {
|
|
254
|
+
// Attach runtime metadata with class names resolved
|
|
255
|
+
attachRuntimeMeta(implementation, {
|
|
256
|
+
invoke: {
|
|
257
|
+
src: service.src,
|
|
258
|
+
onDone: service.onDone.name || service.onDone.toString(),
|
|
259
|
+
onError: service.onError.name || service.onError.toString(),
|
|
260
|
+
description: service.description
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
171
264
|
return implementation as any;
|
|
172
265
|
}
|
|
173
266
|
|
|
@@ -187,5 +280,47 @@ export function action<
|
|
|
187
280
|
action: ActionMeta,
|
|
188
281
|
transition: WithMeta<F, M>
|
|
189
282
|
): WithMeta<F, M & { actions: [typeof action] }> {
|
|
283
|
+
// Attach runtime metadata
|
|
284
|
+
// Note: actions is an array, will be merged by attachRuntimeMeta
|
|
285
|
+
attachRuntimeMeta(transition, {
|
|
286
|
+
actions: [action]
|
|
287
|
+
});
|
|
288
|
+
|
|
190
289
|
return transition as any;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Flexible metadata wrapper for functional and type-state patterns.
|
|
294
|
+
*
|
|
295
|
+
* This function allows attaching metadata to values that don't use the class-based
|
|
296
|
+
* MachineBase pattern. It's particularly useful for:
|
|
297
|
+
* - Functional machines created with createMachine()
|
|
298
|
+
* - Type-state discriminated unions
|
|
299
|
+
* - Generic machine configurations
|
|
300
|
+
*
|
|
301
|
+
* @param meta - Partial metadata object describing states, transitions, etc.
|
|
302
|
+
* @param value - The value to annotate (machine, config, factory function, etc.)
|
|
303
|
+
* @returns The value unchanged (identity function at runtime)
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* // Annotate a functional machine
|
|
307
|
+
* const machine = metadata(
|
|
308
|
+
* {
|
|
309
|
+
* target: IdleState,
|
|
310
|
+
* description: "Counter machine with increment/decrement"
|
|
311
|
+
* },
|
|
312
|
+
* createMachine({ count: 0 }, { ... })
|
|
313
|
+
* );
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* // Annotate a factory function
|
|
317
|
+
* export const createCounter = metadata(
|
|
318
|
+
* { description: "Creates a counter starting at 0" },
|
|
319
|
+
* () => createMachine({ count: 0 }, { ... })
|
|
320
|
+
* );
|
|
321
|
+
*/
|
|
322
|
+
export function metadata<T>(_meta: Partial<TransitionMeta>, value: T): T {
|
|
323
|
+
// At runtime, this is a no-op identity function
|
|
324
|
+
// At compile-time/static-analysis, the metadata can be extracted from the type signature
|
|
325
|
+
return value;
|
|
191
326
|
}
|
package/src/react.ts
CHANGED
|
@@ -1,44 +1,365 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file React integration for @doeixd/machine
|
|
3
|
-
* @description
|
|
3
|
+
* @description
|
|
4
|
+
* Provides a suite of hooks for integrating state machines with React components,
|
|
5
|
+
* covering simple component state, performance-optimized selections, and advanced
|
|
6
|
+
* framework-agnostic patterns.
|
|
7
|
+
*
|
|
8
|
+
* ---
|
|
9
|
+
*
|
|
10
|
+
* ### Hooks Overview
|
|
11
|
+
*
|
|
12
|
+
* 1. **`useMachine(machineFactory)`**:
|
|
13
|
+
* - **Best for:** Local, self-contained component state.
|
|
14
|
+
* - **Returns:** `[machine, actions]`
|
|
15
|
+
* - The simplest way to get started. It manages an immutable machine instance
|
|
16
|
+
* and provides a stable `actions` object to trigger transitions.
|
|
17
|
+
*
|
|
18
|
+
* 2. **`useMachineSelector(machine, selector, isEqual?)`**:
|
|
19
|
+
* - **Best for:** Performance optimization in child components.
|
|
20
|
+
* - **Returns:** A selected slice of the machine's state: `T`.
|
|
21
|
+
* - Subscribes a component to only a part of the machine's state, preventing
|
|
22
|
+
* unnecessary re-renders when other parts of the context change.
|
|
23
|
+
*
|
|
24
|
+
* 3. **`useEnsemble(initialContext, factories, getDiscriminant)`**:
|
|
25
|
+
* - **Best for:** Complex state, shared state, or integrating with external logic.
|
|
26
|
+
* - **Returns:** A stable `Ensemble` instance.
|
|
27
|
+
* - The most powerful hook. It uses the `Ensemble` pattern to decouple your
|
|
28
|
+
* pure machine logic from React's state management, making your business
|
|
29
|
+
* logic portable and easy to test.
|
|
30
|
+
*
|
|
31
|
+
* 4. **`createMachineContext()`**:
|
|
32
|
+
* - **Best for:** Avoiding prop-drilling.
|
|
33
|
+
* - **Returns:** A `Provider` and consumer hooks (`useContext`, `useSelector`, etc.).
|
|
34
|
+
* - A utility to provide a machine created with `useMachine` or `useEnsemble` to
|
|
35
|
+
* the entire component tree below it.
|
|
4
36
|
*/
|
|
5
37
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
38
|
+
import {
|
|
39
|
+
useState,
|
|
40
|
+
useRef,
|
|
41
|
+
useEffect,
|
|
42
|
+
useMemo,
|
|
43
|
+
createContext,
|
|
44
|
+
useContext,
|
|
45
|
+
createElement,
|
|
46
|
+
type ReactNode,
|
|
47
|
+
} from 'react';
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
Machine,
|
|
51
|
+
createRunner,
|
|
52
|
+
createEnsemble,
|
|
53
|
+
type Runner,
|
|
54
|
+
type Ensemble,
|
|
55
|
+
type StateStore,
|
|
56
|
+
} from './index';
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// HOOK 1: useMachine (Ergonomic local state)
|
|
60
|
+
// =============================================================================
|
|
8
61
|
|
|
9
62
|
/**
|
|
10
|
-
* React hook for using
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
63
|
+
* A React hook for using a self-contained, immutable state machine within a component.
|
|
64
|
+
* It provides a more ergonomic API than a raw dispatcher by returning a stable `actions`
|
|
65
|
+
* object, similar to the `createRunner` primitive.
|
|
66
|
+
*
|
|
67
|
+
* This is the ideal hook for managing component-level state.
|
|
68
|
+
*
|
|
69
|
+
* @template M - The machine type (can be a union of states).
|
|
70
|
+
* @param machineFactory - A function that creates the initial machine instance.
|
|
71
|
+
* This function is called only once on the initial render.
|
|
72
|
+
* @returns A tuple of `[machine, actions]`, where:
|
|
73
|
+
* - `machine`: The current, reactive machine instance. Its identity changes on
|
|
74
|
+
* every transition, triggering re-renders. Use this for reading state and
|
|
75
|
+
* for type-narrowing.
|
|
76
|
+
* - `actions`: A stable object containing all possible transition methods,
|
|
77
|
+
* pre-bound to update the machine's state.
|
|
14
78
|
*
|
|
15
79
|
* @example
|
|
16
|
-
*
|
|
80
|
+
* ```tsx
|
|
81
|
+
* const [machine, actions] = useMachine(() => createCounterMachine({ count: 0 }));
|
|
17
82
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
83
|
+
* return (
|
|
84
|
+
* <div>
|
|
85
|
+
* <p>Count: {machine.context.count}</p>
|
|
86
|
+
* <button onClick={() => actions.increment()}>Increment</button>
|
|
87
|
+
* <button onClick={() => actions.add(5)}>Add 5</button>
|
|
88
|
+
* </div>
|
|
89
|
+
* );
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @example With Type-State Programming
|
|
93
|
+
* ```tsx
|
|
94
|
+
* const [auth, actions] = useMachine(() => createLoggedOutMachine());
|
|
95
|
+
*
|
|
96
|
+
* return (
|
|
97
|
+
* <div>
|
|
98
|
+
* {auth.context.status === 'loggedOut' && (
|
|
99
|
+
* <button onClick={() => actions.login('user')}>Login</button>
|
|
100
|
+
* )}
|
|
101
|
+
* {auth.context.status === 'loggedIn' && (
|
|
102
|
+
* <p>Welcome, {auth.context.username}!</p>
|
|
103
|
+
* <button onClick={() => actions.logout()}>Logout</button>
|
|
104
|
+
* )}
|
|
105
|
+
* </div>
|
|
106
|
+
* );
|
|
107
|
+
* ```
|
|
20
108
|
*/
|
|
21
|
-
export function useMachine<M extends
|
|
109
|
+
export function useMachine<M extends Machine<any>>(
|
|
22
110
|
machineFactory: () => M
|
|
23
|
-
): [M,
|
|
24
|
-
//
|
|
111
|
+
): [M, Runner<M>['actions']] {
|
|
112
|
+
// useState holds the machine state, triggering re-renders.
|
|
113
|
+
// The factory is passed directly to useState to ensure it's only called once.
|
|
25
114
|
const [machine, setMachine] = useState(machineFactory);
|
|
26
115
|
|
|
27
|
-
//
|
|
28
|
-
|
|
116
|
+
// useMemo creates a stable runner instance that survives re-renders.
|
|
117
|
+
// The runner's job is to hold the *current* machine state and update our
|
|
118
|
+
// React state when a transition occurs.
|
|
119
|
+
const runner = useMemo(
|
|
120
|
+
() => createRunner(machine, (newState) => {
|
|
121
|
+
// This is the magic link: when the runner's internal state changes,
|
|
122
|
+
// we update React's state, causing a re-render.
|
|
123
|
+
setMachine(newState);
|
|
124
|
+
}),
|
|
125
|
+
[] // Empty dependency array ensures the runner is created only once.
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return [machine, runner.actions];
|
|
129
|
+
}
|
|
29
130
|
|
|
30
|
-
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// HOOK 2: useMachineSelector (Performance optimization)
|
|
133
|
+
// =============================================================================
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A hook that subscribes a component to a selected slice of a machine's state.
|
|
137
|
+
*
|
|
138
|
+
* This is a critical performance optimization. It prevents a component from
|
|
139
|
+
* re-rendering if only an irrelevant part of the machine's context has changed.
|
|
140
|
+
* The component will only re-render if the value returned by the `selector` function
|
|
141
|
+
* is different from the previous render.
|
|
142
|
+
*
|
|
143
|
+
* @template M - The machine type.
|
|
144
|
+
* @template T - The type of the selected value.
|
|
145
|
+
* @param machine - The reactive machine instance from `useMachine`.
|
|
146
|
+
* @param selector - A function that takes the current machine state and returns
|
|
147
|
+
* a derived value.
|
|
148
|
+
* @param isEqual - An optional function to compare the previous and next selected
|
|
149
|
+
* values. Defaults to `Object.is` for strict equality checking. Provide your own
|
|
150
|
+
* for deep comparisons of objects or arrays.
|
|
151
|
+
* @returns The selected, memoized value from the machine's state.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* // In parent component:
|
|
156
|
+
* const [machine, actions] = useMachine(() => createUserMachine());
|
|
157
|
+
*
|
|
158
|
+
* // In child component (only re-renders when the user's name changes):
|
|
159
|
+
* function UserNameDisplay({ machine }) {
|
|
160
|
+
* const userName = useMachineSelector(
|
|
161
|
+
* machine,
|
|
162
|
+
* (m) => m.context.user.name
|
|
163
|
+
* );
|
|
164
|
+
* return <p>User: {userName}</p>;
|
|
165
|
+
* }
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function useMachineSelector<M extends Machine<any>, T>(
|
|
169
|
+
machine: M,
|
|
170
|
+
selector: (state: M) => T,
|
|
171
|
+
isEqual: (a: T, b: T) => boolean = Object.is
|
|
172
|
+
): T {
|
|
173
|
+
// Store the selected value in local state.
|
|
174
|
+
const [selectedValue, setSelectedValue] = useState(() => selector(machine));
|
|
175
|
+
|
|
176
|
+
// Keep refs to the latest selector and comparison functions.
|
|
177
|
+
const selectorRef = useRef(selector);
|
|
178
|
+
const isEqualRef = useRef(isEqual);
|
|
179
|
+
selectorRef.current = selector;
|
|
180
|
+
isEqualRef.current = isEqual;
|
|
181
|
+
|
|
182
|
+
// Effect to update the selected value only when it actually changes.
|
|
31
183
|
useEffect(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
}, []); //
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
184
|
+
const nextValue = selectorRef.current(machine);
|
|
185
|
+
if (!isEqualRef.current(selectedValue, nextValue)) {
|
|
186
|
+
setSelectedValue(nextValue);
|
|
187
|
+
}
|
|
188
|
+
}, [machine, selectedValue]); // Re-run only when the machine or the slice changes.
|
|
189
|
+
|
|
190
|
+
return selectedValue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// =============================================================================
|
|
194
|
+
// HOOK 3: useEnsemble (Advanced integration pattern)
|
|
195
|
+
// =============================================================================
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A hook that creates and manages an `Ensemble` within a React component.
|
|
199
|
+
*
|
|
200
|
+
* This is the most powerful and flexible integration pattern. It decouples your
|
|
201
|
+
* state logic (defined in `factories`) from React's state management. Your machine
|
|
202
|
+
* logic becomes pure, portable, and easily testable outside of React.
|
|
203
|
+
*
|
|
204
|
+
* @template C - The shared context object type.
|
|
205
|
+
* @template F - An object of factory functions that create machine instances.
|
|
206
|
+
* @param initialContext - The initial context object for the machine.
|
|
207
|
+
* @param factories - An object mapping state names to factory functions.
|
|
208
|
+
* @param getDiscriminant - An accessor function that determines the current state
|
|
209
|
+
* from the context.
|
|
210
|
+
* @returns A stable `Ensemble` instance. The component will reactively update
|
|
211
|
+
* when the ensemble's underlying context changes.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```tsx
|
|
215
|
+
* const fetchFactories = {
|
|
216
|
+
* idle: (ctx) => createMachine(ctx, { fetch: () => ({ ...ctx, status: 'loading' }) }),
|
|
217
|
+
* loading: (ctx) => createMachine(ctx, { succeed: (data) => ({ status: 'success', data }) }),
|
|
218
|
+
* // ...
|
|
219
|
+
* };
|
|
220
|
+
*
|
|
221
|
+
* function MyComponent() {
|
|
222
|
+
* const ensemble = useEnsemble(
|
|
223
|
+
* { status: 'idle', data: null },
|
|
224
|
+
* fetchFactories,
|
|
225
|
+
* (ctx) => ctx.status
|
|
226
|
+
* );
|
|
227
|
+
*
|
|
228
|
+
* return (
|
|
229
|
+
* <div>
|
|
230
|
+
* <p>Status: {ensemble.context.status}</p>
|
|
231
|
+
* {ensemble.state.context.status === 'idle' && (
|
|
232
|
+
* <button onClick={() => ensemble.actions.fetch()}>Fetch</button>
|
|
233
|
+
* )}
|
|
234
|
+
* </div>
|
|
235
|
+
* );
|
|
236
|
+
* }
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export function useEnsemble<
|
|
240
|
+
C extends object,
|
|
241
|
+
F extends Record<string, (context: C) => Machine<C>>
|
|
242
|
+
>(
|
|
243
|
+
initialContext: C,
|
|
244
|
+
factories: F,
|
|
245
|
+
getDiscriminant: (context: C) => keyof F
|
|
246
|
+
): Ensemble<ReturnType<F[keyof F]>, C> {
|
|
247
|
+
const [context, setContext] = useState(initialContext);
|
|
248
|
+
const contextRef = useRef(context);
|
|
249
|
+
contextRef.current = context;
|
|
250
|
+
|
|
251
|
+
const store = useMemo<StateStore<C>>(
|
|
252
|
+
() => ({
|
|
253
|
+
// getContext reads from the ref to ensure it always has the latest value,
|
|
254
|
+
// avoiding stale closures.
|
|
255
|
+
getContext: () => contextRef.current,
|
|
256
|
+
setContext: (newContext) => {
|
|
257
|
+
// The update is dispatched to React's state setter.
|
|
258
|
+
setContext(newContext);
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
[] // The store itself is stable and created only once.
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// The ensemble instance is also memoized to remain stable across re-renders.
|
|
265
|
+
const ensemble = useMemo(
|
|
266
|
+
() => createEnsemble(store, factories, getDiscriminant),
|
|
267
|
+
[store, factories, getDiscriminant]
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return ensemble;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// =============================================================================
|
|
274
|
+
// UTILITY 4: createMachineContext (Dependency injection)
|
|
275
|
+
// =============================================================================
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Creates a React Context for providing a machine instance down the component tree,
|
|
279
|
+
* avoiding the need to pass it down as props ("prop-drilling").
|
|
280
|
+
*
|
|
281
|
+
* It returns a `Provider` component and a suite of consumer hooks for accessing
|
|
282
|
+
* the state and actions.
|
|
283
|
+
*
|
|
284
|
+
* @returns An object containing:
|
|
285
|
+
* - `Provider`: The context provider component.
|
|
286
|
+
* - `useMachineContext`: Hook to get the full `[machine, actions]` tuple.
|
|
287
|
+
* - `useMachineState`: Hook to get only the reactive `machine` instance.
|
|
288
|
+
* - `useMachineActions`: Hook to get only the stable `actions` object.
|
|
289
|
+
* - `useSelector`: Hook to get a memoized slice of the machine's state.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```tsx
|
|
293
|
+
* // 1. Create the context
|
|
294
|
+
* const { Provider, useMachineState, useMachineActions } = createMachineContext<MyMachine>();
|
|
295
|
+
*
|
|
296
|
+
* // 2. In your top-level component, create the machine and provide it
|
|
297
|
+
* function App() {
|
|
298
|
+
* const [machine, actions] = useMachine(() => createMyMachine());
|
|
299
|
+
* return (
|
|
300
|
+
* <Provider machine={machine} actions={actions}>
|
|
301
|
+
* <ChildComponent />
|
|
302
|
+
* </Provider>
|
|
303
|
+
* );
|
|
304
|
+
* }
|
|
305
|
+
*
|
|
306
|
+
* // 3. In a deeply nested child component
|
|
307
|
+
* function ChildComponent() {
|
|
308
|
+
* const machine = useMachineState(); // Gets the current state
|
|
309
|
+
* const actions = useMachineActions(); // Gets the stable actions
|
|
310
|
+
* const name = useSelector(m => m.context.name); // Selects a slice
|
|
311
|
+
*
|
|
312
|
+
* return (
|
|
313
|
+
* <div>
|
|
314
|
+
* <p>Name: {name}</p>
|
|
315
|
+
* <button onClick={() => actions.rename('new name')}>Rename</button>
|
|
316
|
+
* </div>
|
|
317
|
+
* );
|
|
318
|
+
* }
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
export function createMachineContext<M extends Machine<any>>() {
|
|
322
|
+
type MachineContextValue = [M, Runner<M>['actions']];
|
|
323
|
+
const Context = createContext<MachineContextValue | null>(null);
|
|
324
|
+
|
|
325
|
+
const Provider = ({
|
|
326
|
+
machine,
|
|
327
|
+
actions,
|
|
328
|
+
children,
|
|
329
|
+
}: {
|
|
330
|
+
machine: M;
|
|
331
|
+
actions: Runner<M>['actions'];
|
|
332
|
+
children: ReactNode;
|
|
333
|
+
}) => {
|
|
334
|
+
// Memoize the context value to prevent unnecessary re-renders in consumers.
|
|
335
|
+
const value = useMemo<MachineContextValue>(() => [machine, actions], [machine, actions]);
|
|
336
|
+
return createElement(Context.Provider, { value }, children);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const useMachineContext = (): MachineContextValue => {
|
|
340
|
+
const context = useContext(Context);
|
|
341
|
+
if (!context) {
|
|
342
|
+
throw new Error('useMachineContext must be used within a Machine.Provider');
|
|
343
|
+
}
|
|
344
|
+
return context;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const useMachineState = (): M => useMachineContext()[0];
|
|
348
|
+
const useMachineActions = (): Runner<M>['actions'] => useMachineContext()[1];
|
|
349
|
+
|
|
350
|
+
const useSelector = <T,>(
|
|
351
|
+
selector: (state: M) => T,
|
|
352
|
+
isEqual?: (a: T, b: T) => boolean
|
|
353
|
+
): T => {
|
|
354
|
+
const machine = useMachineState();
|
|
355
|
+
return useMachineSelector(machine, selector, isEqual);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
Provider,
|
|
360
|
+
useMachineContext,
|
|
361
|
+
useMachineState,
|
|
362
|
+
useMachineActions,
|
|
363
|
+
useSelector,
|
|
364
|
+
};
|
|
44
365
|
}
|