@xstate/react 7.0.0-alpha.2 → 7.0.0-alpha.4

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 CHANGED
@@ -44,3 +44,9 @@ export const Toggler = () => {
44
44
  );
45
45
  };
46
46
  ```
47
+
48
+ ## Observing an actor
49
+
50
+ <!-- observer lifecycle from src/useActorRef.ts -->
51
+
52
+ Pass a snapshot listener or observer as the third argument to `useActorRef(logic, options, observer)`. It subscribes before the actor starts. When the hook replaces the actor (after an external stop, or a Fast Refresh that cannot keep it), the observer follows the replacement, even when the callback identity stays the same. Unmounting unsubscribes the observer and stops the actor.
@@ -1,6 +1,4 @@
1
- import { Actor, ActorOptions, AnyActorLogic, SnapshotFrom, type ConditionalRequired, type IsNotNever, type RequiredActorOptionsKeys } from 'xstate';
1
+ import { Actor, ActorOptions, AnyActorLogic, SnapshotFrom, type ConditionalRequired, type IsNotNever, type RequiredActorOptionsKeys, type RequiredActorOptionsFor } from 'xstate';
2
2
  export declare function useActor<TLogic extends AnyActorLogic>(logic: TLogic, ...[options]: ConditionalRequired<[
3
- options?: ActorOptions<TLogic> & {
4
- [K in RequiredActorOptionsKeys<TLogic>]: unknown;
5
- }
3
+ options?: ActorOptions<TLogic> & RequiredActorOptionsFor<TLogic>
6
4
  ], IsNotNever<RequiredActorOptionsKeys<TLogic>>>): [SnapshotFrom<TLogic>, Actor<TLogic>['send'], Actor<TLogic>];
@@ -1,14 +1,10 @@
1
- import { Actor, ActorOptions, AnyActorLogic, Observer, SnapshotFrom, type ConditionalRequired, type IsNotNever, type RequiredActorOptionsKeys } from 'xstate';
1
+ import { Actor, ActorOptions, AnyActorLogic, Observer, SnapshotFrom, type ConditionalRequired, type IsNotNever, type RequiredActorOptionsKeys, type RequiredActorOptionsFor } from 'xstate';
2
2
  export declare function useIdleActorRef<TLogic extends AnyActorLogic>(logic: TLogic, ...[options]: ConditionalRequired<[
3
- options?: ActorOptions<TLogic> & {
4
- [K in RequiredActorOptionsKeys<TLogic>]: unknown;
5
- }
3
+ options?: ActorOptions<TLogic> & RequiredActorOptionsFor<TLogic>
6
4
  ], IsNotNever<RequiredActorOptionsKeys<TLogic>>>): [Actor<TLogic>, (actorRef: Actor<TLogic>) => void];
7
5
  export declare function useActorLifecycle<TLogic extends AnyActorLogic>(actorRef: Actor<TLogic>, setActorRef: (actorRef: Actor<TLogic>) => void, createReplacement: () => Actor<TLogic>): void;
8
6
  export declare function useActorRef<TLogic extends AnyActorLogic>(machine: TLogic, ...[options, observerOrListener]: IsNotNever<RequiredActorOptionsKeys<TLogic>> extends true ? [
9
- options: ActorOptions<TLogic> & {
10
- [K in RequiredActorOptionsKeys<TLogic>]: unknown;
11
- },
7
+ options: ActorOptions<TLogic> & RequiredActorOptionsFor<TLogic>,
12
8
  observerOrListener?: Observer<SnapshotFrom<TLogic>> | ((value: SnapshotFrom<TLogic>) => void)
13
9
  ] : [
14
10
  options?: ActorOptions<TLogic>,
@@ -1,7 +1,5 @@
1
- import { Actor, ActorOptions, AnyStateMachine, StateFrom, type ConditionalRequired, type IsNotNever, type RequiredActorOptionsKeys } from 'xstate';
1
+ import { Actor, ActorOptions, AnyStateMachine, StateFrom, type ConditionalRequired, type IsNotNever, type RequiredActorOptionsKeys, type RequiredActorOptionsFor } from 'xstate';
2
2
  /** @alias useActor */
3
3
  export declare function useMachine<TMachine extends AnyStateMachine>(machine: TMachine, ...[options]: ConditionalRequired<[
4
- options?: ActorOptions<TMachine> & {
5
- [K in RequiredActorOptionsKeys<TMachine>]: unknown;
6
- }
4
+ options?: ActorOptions<TMachine> & RequiredActorOptionsFor<TMachine>
7
5
  ], IsNotNever<RequiredActorOptionsKeys<TMachine>>>): [StateFrom<TMachine>, Actor<TMachine>['send'], Actor<TMachine>];
@@ -32,23 +32,28 @@ var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
32
  var useIsomorphicLayoutEffect__default = /*#__PURE__*/_interopDefault(useIsomorphicLayoutEffect);
33
33
 
34
34
  function useIdleActorRef(logic, ...[options]) {
35
- let [actorRef, setActorRef] = React.useState(() => {
35
+ const [actorRef, setActorRef] = React.useState(() => {
36
36
  return xstate.createActor(logic, options);
37
37
  });
38
- if (logic.config !== actorRef.logic.config) {
39
- const newActorRef = xstate.createActor(logic, {
40
- ...options,
41
- snapshot: actorRef.getPersistedSnapshot({
42
- __unsafeAllowInlineActors: true
43
- })
44
- });
45
- setActorRef(newActorRef);
46
- actorRef = newActorRef;
47
- }
38
+ // An object whose identity changes only when React Fast Refresh re-renders
39
+ // this component. Fast Refresh ignores dependency lists while it applies an
40
+ // update, so a `useMemo` with no dependencies recomputes during a refresh
41
+ // and at no other time.
42
+ const refreshSignal = React.useMemo(() => ({}), []);
43
+ const refreshSignalRef = React.useRef(refreshSignal);
48
44
 
45
+ // The logic passed on the first render is used for the hook's lifetime.
46
+ // Later renders only contribute implementations provided with
47
+ // `machine.provide()` for the same machine config.
49
48
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
50
49
  useIsomorphicLayoutEffect__default["default"](() => {
51
- actorRef.logic.sources = logic.sources;
50
+ const currentLogic = actorRef.logic;
51
+ refreshSignalRef.current !== refreshSignal;
52
+ refreshSignalRef.current = refreshSignal;
53
+ if (logic.config === currentLogic.config) {
54
+ currentLogic.sources = logic.sources;
55
+ return;
56
+ }
52
57
  });
53
58
  return [actorRef, setActorRef];
54
59
  }
@@ -65,7 +70,6 @@ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
65
70
  // actor. A stopped actor cannot be restarted.
66
71
  if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
67
72
  const newActor = createReplacement();
68
- newActor.start();
69
73
  setActorRef(newActor);
70
74
  return;
71
75
  }
@@ -97,8 +101,8 @@ function useActorRef(machine, ...[options, observerOrListener]) {
97
101
  return () => {
98
102
  sub.unsubscribe();
99
103
  };
100
- }, [observerOrListener]);
101
- useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(machine, actorRef.options));
104
+ }, [actorRef, observerOrListener]);
105
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(actorRef.logic, actorRef.options));
102
106
  return actorRef;
103
107
  }
104
108
 
@@ -214,7 +218,7 @@ function useActor(logic, ...[options]) {
214
218
  if (snapshotWithStatus?.status === 'error') {
215
219
  throw snapshotWithStatus.error;
216
220
  }
217
- useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(logic, options));
221
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(actorRef.logic, options));
218
222
  return [actorSnapshot, actorRef.send, actorRef];
219
223
  }
220
224
 
@@ -32,23 +32,37 @@ var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
32
  var useIsomorphicLayoutEffect__default = /*#__PURE__*/_interopDefault(useIsomorphicLayoutEffect);
33
33
 
34
34
  function useIdleActorRef(logic, ...[options]) {
35
- let [actorRef, setActorRef] = React.useState(() => {
35
+ const [actorRef, setActorRef] = React.useState(() => {
36
36
  return xstate.createActor(logic, options);
37
37
  });
38
- if (logic.config !== actorRef.logic.config) {
39
- const newActorRef = xstate.createActor(logic, {
40
- ...options,
41
- snapshot: actorRef.getPersistedSnapshot({
42
- __unsafeAllowInlineActors: true
43
- })
44
- });
45
- setActorRef(newActorRef);
46
- actorRef = newActorRef;
47
- }
38
+ // An object whose identity changes only when React Fast Refresh re-renders
39
+ // this component. Fast Refresh ignores dependency lists while it applies an
40
+ // update, so a `useMemo` with no dependencies recomputes during a refresh
41
+ // and at no other time.
42
+ const refreshSignal = React.useMemo(() => ({}), []);
43
+ const refreshSignalRef = React.useRef(refreshSignal);
48
44
 
45
+ // The logic passed on the first render is used for the hook's lifetime.
46
+ // Later renders only contribute implementations provided with
47
+ // `machine.provide()` for the same machine config.
49
48
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
50
49
  useIsomorphicLayoutEffect__default["default"](() => {
51
- actorRef.logic.sources = logic.sources;
50
+ const currentLogic = actorRef.logic;
51
+ const refreshed = refreshSignalRef.current !== refreshSignal;
52
+ refreshSignalRef.current = refreshSignal;
53
+ if (logic.config === currentLogic.config) {
54
+ currentLogic.sources = logic.sources;
55
+ return;
56
+ }
57
+
58
+ // Development hot reloading: Fast Refresh re-evaluated the module that
59
+ // defines the machine. Keep the running actor and carry its live snapshot
60
+ // over to the new machine; start a fresh actor if it cannot be carried.
61
+ if (refreshed) {
62
+ if (!xstate._hotSwapActorLogic(actorRef, logic)) {
63
+ setActorRef(xstate.createActor(logic, options));
64
+ }
65
+ }
52
66
  });
53
67
  return [actorRef, setActorRef];
54
68
  }
@@ -65,7 +79,6 @@ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
65
79
  // actor. A stopped actor cannot be restarted.
66
80
  if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
67
81
  const newActor = createReplacement();
68
- newActor.start();
69
82
  setActorRef(newActor);
70
83
  return;
71
84
  }
@@ -97,8 +110,8 @@ function useActorRef(machine, ...[options, observerOrListener]) {
97
110
  return () => {
98
111
  sub.unsubscribe();
99
112
  };
100
- }, [observerOrListener]);
101
- useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(machine, actorRef.options));
113
+ }, [actorRef, observerOrListener]);
114
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(actorRef.logic, actorRef.options));
102
115
  return actorRef;
103
116
  }
104
117
 
@@ -217,7 +230,7 @@ function useActor(logic, ...[options]) {
217
230
  if (snapshotWithStatus?.status === 'error') {
218
231
  throw snapshotWithStatus.error;
219
232
  }
220
- useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(logic, options));
233
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(actorRef.logic, options));
221
234
  return [actorSnapshot, actorRef.send, actorRef];
222
235
  }
223
236
 
@@ -1,28 +1,42 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useState, useRef, useCallback } from 'react';
2
+ import { useEffect, useState, useMemo, useRef, useCallback } from 'react';
3
3
  import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect';
4
- import { toObserver, createActor } from 'xstate';
4
+ import { toObserver, createActor, _hotSwapActorLogic } from 'xstate';
5
5
  import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
6
6
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
7
7
 
8
8
  function useIdleActorRef(logic, ...[options]) {
9
- let [actorRef, setActorRef] = useState(() => {
9
+ const [actorRef, setActorRef] = useState(() => {
10
10
  return createActor(logic, options);
11
11
  });
12
- if (logic.config !== actorRef.logic.config) {
13
- const newActorRef = createActor(logic, {
14
- ...options,
15
- snapshot: actorRef.getPersistedSnapshot({
16
- __unsafeAllowInlineActors: true
17
- })
18
- });
19
- setActorRef(newActorRef);
20
- actorRef = newActorRef;
21
- }
12
+ // An object whose identity changes only when React Fast Refresh re-renders
13
+ // this component. Fast Refresh ignores dependency lists while it applies an
14
+ // update, so a `useMemo` with no dependencies recomputes during a refresh
15
+ // and at no other time.
16
+ const refreshSignal = useMemo(() => ({}), []);
17
+ const refreshSignalRef = useRef(refreshSignal);
22
18
 
19
+ // The logic passed on the first render is used for the hook's lifetime.
20
+ // Later renders only contribute implementations provided with
21
+ // `machine.provide()` for the same machine config.
23
22
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
24
23
  useIsomorphicLayoutEffect(() => {
25
- actorRef.logic.sources = logic.sources;
24
+ const currentLogic = actorRef.logic;
25
+ const refreshed = refreshSignalRef.current !== refreshSignal;
26
+ refreshSignalRef.current = refreshSignal;
27
+ if (logic.config === currentLogic.config) {
28
+ currentLogic.sources = logic.sources;
29
+ return;
30
+ }
31
+
32
+ // Development hot reloading: Fast Refresh re-evaluated the module that
33
+ // defines the machine. Keep the running actor and carry its live snapshot
34
+ // over to the new machine; start a fresh actor if it cannot be carried.
35
+ if (refreshed) {
36
+ if (!_hotSwapActorLogic(actorRef, logic)) {
37
+ setActorRef(createActor(logic, options));
38
+ }
39
+ }
26
40
  });
27
41
  return [actorRef, setActorRef];
28
42
  }
@@ -39,7 +53,6 @@ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
39
53
  // actor. A stopped actor cannot be restarted.
40
54
  if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
41
55
  const newActor = createReplacement();
42
- newActor.start();
43
56
  setActorRef(newActor);
44
57
  return;
45
58
  }
@@ -71,8 +84,8 @@ function useActorRef(machine, ...[options, observerOrListener]) {
71
84
  return () => {
72
85
  sub.unsubscribe();
73
86
  };
74
- }, [observerOrListener]);
75
- useActorLifecycle(actorRef, setActorRef, () => createActor(machine, actorRef.options));
87
+ }, [actorRef, observerOrListener]);
88
+ useActorLifecycle(actorRef, setActorRef, () => createActor(actorRef.logic, actorRef.options));
76
89
  return actorRef;
77
90
  }
78
91
 
@@ -191,7 +204,7 @@ function useActor(logic, ...[options]) {
191
204
  if (snapshotWithStatus?.status === 'error') {
192
205
  throw snapshotWithStatus.error;
193
206
  }
194
- useActorLifecycle(actorRef, setActorRef, () => createActor(logic, options));
207
+ useActorLifecycle(actorRef, setActorRef, () => createActor(actorRef.logic, options));
195
208
  return [actorSnapshot, actorRef.send, actorRef];
196
209
  }
197
210
 
@@ -1,28 +1,33 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useState, useRef, useCallback } from 'react';
2
+ import { useEffect, useState, useMemo, useRef, useCallback } from 'react';
3
3
  import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect';
4
4
  import { toObserver, createActor } from 'xstate';
5
5
  import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
6
6
  import { useSyncExternalStore } from 'use-sync-external-store/shim';
7
7
 
8
8
  function useIdleActorRef(logic, ...[options]) {
9
- let [actorRef, setActorRef] = useState(() => {
9
+ const [actorRef, setActorRef] = useState(() => {
10
10
  return createActor(logic, options);
11
11
  });
12
- if (logic.config !== actorRef.logic.config) {
13
- const newActorRef = createActor(logic, {
14
- ...options,
15
- snapshot: actorRef.getPersistedSnapshot({
16
- __unsafeAllowInlineActors: true
17
- })
18
- });
19
- setActorRef(newActorRef);
20
- actorRef = newActorRef;
21
- }
12
+ // An object whose identity changes only when React Fast Refresh re-renders
13
+ // this component. Fast Refresh ignores dependency lists while it applies an
14
+ // update, so a `useMemo` with no dependencies recomputes during a refresh
15
+ // and at no other time.
16
+ const refreshSignal = useMemo(() => ({}), []);
17
+ const refreshSignalRef = useRef(refreshSignal);
22
18
 
19
+ // The logic passed on the first render is used for the hook's lifetime.
20
+ // Later renders only contribute implementations provided with
21
+ // `machine.provide()` for the same machine config.
23
22
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
24
23
  useIsomorphicLayoutEffect(() => {
25
- actorRef.logic.sources = logic.sources;
24
+ const currentLogic = actorRef.logic;
25
+ refreshSignalRef.current !== refreshSignal;
26
+ refreshSignalRef.current = refreshSignal;
27
+ if (logic.config === currentLogic.config) {
28
+ currentLogic.sources = logic.sources;
29
+ return;
30
+ }
26
31
  });
27
32
  return [actorRef, setActorRef];
28
33
  }
@@ -39,7 +44,6 @@ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
39
44
  // actor. A stopped actor cannot be restarted.
40
45
  if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
41
46
  const newActor = createReplacement();
42
- newActor.start();
43
47
  setActorRef(newActor);
44
48
  return;
45
49
  }
@@ -71,8 +75,8 @@ function useActorRef(machine, ...[options, observerOrListener]) {
71
75
  return () => {
72
76
  sub.unsubscribe();
73
77
  };
74
- }, [observerOrListener]);
75
- useActorLifecycle(actorRef, setActorRef, () => createActor(machine, actorRef.options));
78
+ }, [actorRef, observerOrListener]);
79
+ useActorLifecycle(actorRef, setActorRef, () => createActor(actorRef.logic, actorRef.options));
76
80
  return actorRef;
77
81
  }
78
82
 
@@ -188,7 +192,7 @@ function useActor(logic, ...[options]) {
188
192
  if (snapshotWithStatus?.status === 'error') {
189
193
  throw snapshotWithStatus.error;
190
194
  }
191
- useActorLifecycle(actorRef, setActorRef, () => createActor(logic, options));
195
+ useActorLifecycle(actorRef, setActorRef, () => createActor(actorRef.logic, options));
192
196
  return [actorSnapshot, actorRef.send, actorRef];
193
197
  }
194
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xstate/react",
3
- "version": "7.0.0-alpha.2",
3
+ "version": "7.0.0-alpha.4",
4
4
  "description": "XState tools for React",
5
5
  "keywords": [
6
6
  "state",
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "peerDependencies": {
56
56
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
57
- "xstate": "^6.0.0-alpha.52"
57
+ "xstate": "^6.0.0-alpha.61"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "xstate": {
@@ -74,7 +74,7 @@
74
74
  "react-dom": "^19.0.0",
75
75
  "rxjs": "^7.8.1",
76
76
  "zod": "^3.25.51",
77
- "xstate": "^6.0.0-alpha.52"
77
+ "xstate": "^6.0.0-alpha.61"
78
78
  },
79
79
  "scripts": {}
80
80
  }