@doeixd/machine 0.0.19 → 0.0.21

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/src/react.ts CHANGED
@@ -33,6 +33,10 @@
33
33
  * - **Returns:** A `Provider` and consumer hooks (`useContext`, `useSelector`, etc.).
34
34
  * - A utility to provide a machine created with `useMachine` or `useEnsemble` to
35
35
  * the entire component tree below it.
36
+ *
37
+ * 5. **`useActor(actor)`**:
38
+ * - **Best for:** Using the Actor model.
39
+ * - **Returns:** The current machine snapshot.
36
40
  */
37
41
 
38
42
  import {
@@ -43,18 +47,22 @@ import {
43
47
  createContext,
44
48
  useContext,
45
49
  createElement,
50
+ useSyncExternalStore,
46
51
  type ReactNode,
47
52
  } from 'react';
48
53
 
49
54
  import {
50
55
  Machine,
51
- createRunner,
56
+ runMachine, // Was createRunner
52
57
  createEnsemble,
53
- type Runner,
54
58
  type Ensemble,
55
59
  type StateStore,
60
+ type Actor,
61
+ BaseMachine
56
62
  } from './index';
57
63
 
64
+ export type Runner<M extends Machine<any>> = ReturnType<typeof runMachine<M>>;
65
+
58
66
  // =============================================================================
59
67
  // HOOK 1: useMachine (Ergonomic local state)
60
68
  // =============================================================================
@@ -62,7 +70,7 @@ import {
62
70
  /**
63
71
  * A React hook for using a self-contained, immutable state machine within a component.
64
72
  * It provides a more ergonomic API than a raw dispatcher by returning a stable `actions`
65
- * object, similar to the `createRunner` primitive.
73
+ * object, similar to the `runMachine` primitive.
66
74
  *
67
75
  * This is the ideal hook for managing component-level state.
68
76
  *
@@ -75,57 +83,33 @@ import {
75
83
  * for type-narrowing.
76
84
  * - `actions`: A stable object containing all possible transition methods,
77
85
  * pre-bound to update the machine's state.
78
- *
79
- * @example
80
- * ```tsx
81
- * const [machine, actions] = useMachine(() => createCounterMachine({ count: 0 }));
82
- *
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
- * ```
108
86
  */
109
87
  export function useMachine<M extends Machine<any>>(
110
88
  machineFactory: () => M
111
- ): [M, Runner<M>['actions']] {
89
+ ): [M, Record<string, (...args: any[]) => void>] {
112
90
  // useState holds the machine state, triggering re-renders.
113
- // The factory is passed directly to useState to ensure it's only called once.
114
91
  const [machine, setMachine] = useState(machineFactory);
115
92
 
116
93
  // 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
94
  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.
95
+ () => runMachine(machine, (newState) => {
123
96
  setMachine(newState);
124
97
  }),
125
- [] // Empty dependency array ensures the runner is created only once.
98
+ []
126
99
  );
127
100
 
128
- return [machine, runner.actions];
101
+ // Create a stable actions object that proxies calls to the dispatcher
102
+ const actions = useMemo(() => {
103
+ return new Proxy({} as any, {
104
+ get: (_target, prop) => {
105
+ return (...args: any[]) => {
106
+ runner.dispatch({ type: prop as any, args: args as any } as any);
107
+ };
108
+ }
109
+ });
110
+ }, [runner]);
111
+
112
+ return [machine, actions];
129
113
  }
130
114
 
131
115
  // =============================================================================
@@ -149,21 +133,6 @@ export function useMachine<M extends Machine<any>>(
149
133
  * values. Defaults to `Object.is` for strict equality checking. Provide your own
150
134
  * for deep comparisons of objects or arrays.
151
135
  * @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
136
  */
168
137
  export function useMachineSelector<M extends Machine<any>, T>(
169
138
  machine: M,
@@ -172,7 +141,7 @@ export function useMachineSelector<M extends Machine<any>, T>(
172
141
  ): T {
173
142
  // Store the selected value in local state.
174
143
  const [selectedValue, setSelectedValue] = useState(() => selector(machine));
175
-
144
+
176
145
  // Keep refs to the latest selector and comparison functions.
177
146
  const selectorRef = useRef(selector);
178
147
  const isEqualRef = useRef(isEqual);
@@ -209,32 +178,6 @@ export function useMachineSelector<M extends Machine<any>, T>(
209
178
  * from the context.
210
179
  * @returns A stable `Ensemble` instance. The component will reactively update
211
180
  * 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
181
  */
239
182
  export function useEnsemble<
240
183
  C extends object,
@@ -280,46 +223,9 @@ export function useEnsemble<
280
223
  *
281
224
  * It returns a `Provider` component and a suite of consumer hooks for accessing
282
225
  * 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
226
  */
321
227
  export function createMachineContext<M extends Machine<any>>() {
322
- type MachineContextValue = [M, Runner<M>['actions']];
228
+ type MachineContextValue = [M, Record<string, (...args: any[]) => void>];
323
229
  const Context = createContext<MachineContextValue | null>(null);
324
230
 
325
231
  const Provider = ({
@@ -328,7 +234,7 @@ export function createMachineContext<M extends Machine<any>>() {
328
234
  children,
329
235
  }: {
330
236
  machine: M;
331
- actions: Runner<M>['actions'];
237
+ actions: Record<string, (...args: any[]) => void>;
332
238
  children: ReactNode;
333
239
  }) => {
334
240
  // Memoize the context value to prevent unnecessary re-renders in consumers.
@@ -345,7 +251,7 @@ export function createMachineContext<M extends Machine<any>>() {
345
251
  };
346
252
 
347
253
  const useMachineState = (): M => useMachineContext()[0];
348
- const useMachineActions = (): Runner<M>['actions'] => useMachineContext()[1];
254
+ const useMachineActions = (): Record<string, (...args: any[]) => void> => useMachineContext()[1];
349
255
 
350
256
  const useSelector = <T,>(
351
257
  selector: (state: M) => T,
@@ -362,4 +268,69 @@ export function createMachineContext<M extends Machine<any>>() {
362
268
  useMachineActions,
363
269
  useSelector,
364
270
  };
271
+ }
272
+
273
+ // =============================================================================
274
+ // HOOK 5: useActor (Actor Model)
275
+ // =============================================================================
276
+
277
+ /**
278
+ * Subscribes to an Actor and returns the current snapshot.
279
+ * Uses `useSyncExternalStore` for concurrent features compatibility.
280
+ *
281
+ * @param actor The actor instance to subscribe to.
282
+ * @returns The current machine snapshot.
283
+ */
284
+ export function useActor<M extends BaseMachine<any>>(actor: Actor<M>): M {
285
+ // bind is important if subscribe methods rely on `this`
286
+ const subscribe = useMemo(() => actor.subscribe.bind(actor), [actor]);
287
+ const getSnapshot = useMemo(() => actor.getSnapshot.bind(actor), [actor]);
288
+
289
+ return useSyncExternalStore(subscribe, getSnapshot);
290
+ }
291
+
292
+ /**
293
+ * Subscribes to an Actor and selects a slice of the state.
294
+ * Only re-renders when the selected slice changes.
295
+ *
296
+ * @param actor The actor instance.
297
+ * @param selector Function to select a part of the state.
298
+ * @param isEqual Optional equality function.
299
+ */
300
+ export function useActorSelector<M extends BaseMachine<any>, T>(
301
+ actor: Actor<M>,
302
+ selector: (state: M) => T,
303
+ isEqual: (a: T, b: T) => boolean = Object.is
304
+ ): T {
305
+ const subscribe = useMemo(() => actor.subscribe.bind(actor), [actor]);
306
+ const getSnapshot = useMemo(() => actor.getSnapshot.bind(actor), [actor]);
307
+
308
+ const getSelection = () => selector(getSnapshot());
309
+
310
+ const [selection, setSelection] = useState(getSelection);
311
+
312
+ // Custom selector logic since useSyncExternalStoreWithSelector is not available directly
313
+ // and we want to avoid extra deps.
314
+ // Actually, we can just use useSyncExternalStore and manage the selection stability,
315
+ // but useSyncExternalStore triggers if the result of getSnapshot changes (strict eq).
316
+ // If we wrap getSnapshot to return the selection, standard useSyncExternalStore handles it?
317
+ // No, useSyncExternalStore calls getSnapshot continuously during render to check for tearing.
318
+ // It needs to be cheap and consistent.
319
+
320
+ // Simple implementation: Subscribe and update local state only on change.
321
+ useEffect(() => {
322
+ const checkUpdate = () => {
323
+ const nextSelection = selector(actor.getSnapshot());
324
+ setSelection(prev => isEqual(prev, nextSelection) ? prev : nextSelection);
325
+ };
326
+
327
+ // Check immediately in case it changed between render and effect
328
+ checkUpdate();
329
+
330
+ return actor.subscribe(() => {
331
+ checkUpdate();
332
+ });
333
+ }, [actor, selector, isEqual]);
334
+
335
+ return selection;
365
336
  }