@xstate/react 6.1.0 → 7.0.0-alpha.2

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
@@ -24,10 +24,10 @@ const toggleMachine = createMachine({
24
24
  initial: 'inactive',
25
25
  states: {
26
26
  inactive: {
27
- on: { TOGGLE: 'active' }
27
+ on: { TOGGLE: { target: 'active' } }
28
28
  },
29
29
  active: {
30
- on: { TOGGLE: 'inactive' }
30
+ on: { TOGGLE: { target: 'inactive' } }
31
31
  }
32
32
  }
33
33
  });
@@ -3,7 +3,8 @@ export declare function useIdleActorRef<TLogic extends AnyActorLogic>(logic: TLo
3
3
  options?: ActorOptions<TLogic> & {
4
4
  [K in RequiredActorOptionsKeys<TLogic>]: unknown;
5
5
  }
6
- ], IsNotNever<RequiredActorOptionsKeys<TLogic>>>): Actor<TLogic>;
6
+ ], IsNotNever<RequiredActorOptionsKeys<TLogic>>>): [Actor<TLogic>, (actorRef: Actor<TLogic>) => void];
7
+ export declare function useActorLifecycle<TLogic extends AnyActorLogic>(actorRef: Actor<TLogic>, setActorRef: (actorRef: Actor<TLogic>) => void, createReplacement: () => Actor<TLogic>): void;
7
8
  export declare function useActorRef<TLogic extends AnyActorLogic>(machine: TLogic, ...[options, observerOrListener]: IsNotNever<RequiredActorOptionsKeys<TLogic>> extends true ? [
8
9
  options: ActorOptions<TLogic> & {
9
10
  [K in RequiredActorOptionsKeys<TLogic>]: unknown;
@@ -31,62 +31,64 @@ function _interopNamespace(e) {
31
31
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
32
  var useIsomorphicLayoutEffect__default = /*#__PURE__*/_interopDefault(useIsomorphicLayoutEffect);
33
33
 
34
- const forEachActor = (actorRef, callback) => {
35
- callback(actorRef);
36
- const children = actorRef.getSnapshot().children;
37
- if (children) {
38
- Object.values(children).forEach(child => {
39
- forEachActor(child, callback);
40
- });
41
- }
42
- };
43
- function stopRootWithRehydration(actorRef) {
44
- // persist snapshot here in a custom way allows us to persist inline actors and to preserve actor references
45
- // we do it to avoid setState in useEffect when the effect gets "reconnected"
46
- // this currently only happens in Strict Effects but it simulates the Offscreen aka Activity API
47
- // it also just allows us to end up with a somewhat more predictable behavior for the users
48
- const persistedSnapshots = [];
49
- forEachActor(actorRef, ref => {
50
- persistedSnapshots.push([ref, ref.getSnapshot()]);
51
- // muting observers allow us to avoid `useSelector` from being notified about the stopped snapshot
52
- // React reconnects its subscribers (from the useSyncExternalStore) on its own
53
- // and userland subscribers should basically always do the same anyway
54
- // as each subscription should have its own cleanup logic and that should be called each such reconnect
55
- ref.observers = new Set();
56
- });
57
- const systemSnapshot = actorRef.system.getSnapshot?.();
58
- actorRef.stop();
59
- actorRef.system._snapshot = systemSnapshot;
60
- persistedSnapshots.forEach(([ref, snapshot]) => {
61
- ref._processingStatus = 0;
62
- ref._snapshot = snapshot;
63
- });
64
- }
65
-
66
34
  function useIdleActorRef(logic, ...[options]) {
67
- let [[currentConfig, actorRef], setCurrent] = React.useState(() => {
68
- const actorRef = xstate.createActor(logic, options);
69
- return [logic.config, actorRef];
35
+ let [actorRef, setActorRef] = React.useState(() => {
36
+ return xstate.createActor(logic, options);
70
37
  });
71
- if (logic.config !== currentConfig) {
38
+ if (logic.config !== actorRef.logic.config) {
72
39
  const newActorRef = xstate.createActor(logic, {
73
40
  ...options,
74
41
  snapshot: actorRef.getPersistedSnapshot({
75
42
  __unsafeAllowInlineActors: true
76
43
  })
77
44
  });
78
- setCurrent([logic.config, newActorRef]);
45
+ setActorRef(newActorRef);
79
46
  actorRef = newActorRef;
80
47
  }
81
48
 
82
49
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
83
50
  useIsomorphicLayoutEffect__default["default"](() => {
84
- actorRef.logic.implementations = logic.implementations;
51
+ actorRef.logic.sources = logic.sources;
85
52
  });
86
- return actorRef;
53
+ return [actorRef, setActorRef];
54
+ }
55
+ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
56
+ const pendingStopsRef = React.useRef(new Map());
57
+ React.useEffect(() => {
58
+ const cancelPendingStop = pendingStopsRef.current.get(actorRef);
59
+ if (cancelPendingStop) {
60
+ cancelPendingStop();
61
+ pendingStopsRef.current.delete(actorRef);
62
+ }
63
+
64
+ // If the actor was stopped before this effect reconnected, create a fresh
65
+ // actor. A stopped actor cannot be restarted.
66
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
67
+ const newActor = createReplacement();
68
+ newActor.start();
69
+ setActorRef(newActor);
70
+ return;
71
+ }
72
+ actorRef.start();
73
+ return () => {
74
+ let canceled = false;
75
+ const cancel = () => {
76
+ canceled = true;
77
+ };
78
+ pendingStopsRef.current.set(actorRef, cancel);
79
+ queueMicrotask(() => {
80
+ if (!canceled) {
81
+ actorRef.stop();
82
+ }
83
+ if (pendingStopsRef.current.get(actorRef) === cancel) {
84
+ pendingStopsRef.current.delete(actorRef);
85
+ }
86
+ });
87
+ };
88
+ }, [actorRef]);
87
89
  }
88
90
  function useActorRef(machine, ...[options, observerOrListener]) {
89
- const actorRef = useIdleActorRef(machine, options);
91
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
90
92
  React.useEffect(() => {
91
93
  if (!observerOrListener) {
92
94
  return;
@@ -96,12 +98,7 @@ function useActorRef(machine, ...[options, observerOrListener]) {
96
98
  sub.unsubscribe();
97
99
  };
98
100
  }, [observerOrListener]);
99
- React.useEffect(() => {
100
- actorRef.start();
101
- return () => {
102
- stopRootWithRehydration(actorRef);
103
- };
104
- }, [actorRef]);
101
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(machine, actorRef.options));
105
102
  return actorRef;
106
103
  }
107
104
 
@@ -199,7 +196,7 @@ function shallowEqual(objA, objB) {
199
196
  }
200
197
 
201
198
  function useActor(logic, ...[options]) {
202
- const actorRef = useIdleActorRef(logic, options);
199
+ const [actorRef, setActorRef] = useIdleActorRef(logic, options);
203
200
  const getSnapshot = React.useCallback(() => {
204
201
  return actorRef.getSnapshot();
205
202
  }, [actorRef]);
@@ -217,12 +214,7 @@ function useActor(logic, ...[options]) {
217
214
  if (snapshotWithStatus?.status === 'error') {
218
215
  throw snapshotWithStatus.error;
219
216
  }
220
- React.useEffect(() => {
221
- actorRef.start();
222
- return () => {
223
- stopRootWithRehydration(actorRef);
224
- };
225
- }, [actorRef]);
217
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(logic, options));
226
218
  return [actorSnapshot, actorRef.send, actorRef];
227
219
  }
228
220
 
@@ -31,62 +31,64 @@ function _interopNamespace(e) {
31
31
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
32
  var useIsomorphicLayoutEffect__default = /*#__PURE__*/_interopDefault(useIsomorphicLayoutEffect);
33
33
 
34
- const forEachActor = (actorRef, callback) => {
35
- callback(actorRef);
36
- const children = actorRef.getSnapshot().children;
37
- if (children) {
38
- Object.values(children).forEach(child => {
39
- forEachActor(child, callback);
40
- });
41
- }
42
- };
43
- function stopRootWithRehydration(actorRef) {
44
- // persist snapshot here in a custom way allows us to persist inline actors and to preserve actor references
45
- // we do it to avoid setState in useEffect when the effect gets "reconnected"
46
- // this currently only happens in Strict Effects but it simulates the Offscreen aka Activity API
47
- // it also just allows us to end up with a somewhat more predictable behavior for the users
48
- const persistedSnapshots = [];
49
- forEachActor(actorRef, ref => {
50
- persistedSnapshots.push([ref, ref.getSnapshot()]);
51
- // muting observers allow us to avoid `useSelector` from being notified about the stopped snapshot
52
- // React reconnects its subscribers (from the useSyncExternalStore) on its own
53
- // and userland subscribers should basically always do the same anyway
54
- // as each subscription should have its own cleanup logic and that should be called each such reconnect
55
- ref.observers = new Set();
56
- });
57
- const systemSnapshot = actorRef.system.getSnapshot?.();
58
- actorRef.stop();
59
- actorRef.system._snapshot = systemSnapshot;
60
- persistedSnapshots.forEach(([ref, snapshot]) => {
61
- ref._processingStatus = 0;
62
- ref._snapshot = snapshot;
63
- });
64
- }
65
-
66
34
  function useIdleActorRef(logic, ...[options]) {
67
- let [[currentConfig, actorRef], setCurrent] = React.useState(() => {
68
- const actorRef = xstate.createActor(logic, options);
69
- return [logic.config, actorRef];
35
+ let [actorRef, setActorRef] = React.useState(() => {
36
+ return xstate.createActor(logic, options);
70
37
  });
71
- if (logic.config !== currentConfig) {
38
+ if (logic.config !== actorRef.logic.config) {
72
39
  const newActorRef = xstate.createActor(logic, {
73
40
  ...options,
74
41
  snapshot: actorRef.getPersistedSnapshot({
75
42
  __unsafeAllowInlineActors: true
76
43
  })
77
44
  });
78
- setCurrent([logic.config, newActorRef]);
45
+ setActorRef(newActorRef);
79
46
  actorRef = newActorRef;
80
47
  }
81
48
 
82
49
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
83
50
  useIsomorphicLayoutEffect__default["default"](() => {
84
- actorRef.logic.implementations = logic.implementations;
51
+ actorRef.logic.sources = logic.sources;
85
52
  });
86
- return actorRef;
53
+ return [actorRef, setActorRef];
54
+ }
55
+ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
56
+ const pendingStopsRef = React.useRef(new Map());
57
+ React.useEffect(() => {
58
+ const cancelPendingStop = pendingStopsRef.current.get(actorRef);
59
+ if (cancelPendingStop) {
60
+ cancelPendingStop();
61
+ pendingStopsRef.current.delete(actorRef);
62
+ }
63
+
64
+ // If the actor was stopped before this effect reconnected, create a fresh
65
+ // actor. A stopped actor cannot be restarted.
66
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
67
+ const newActor = createReplacement();
68
+ newActor.start();
69
+ setActorRef(newActor);
70
+ return;
71
+ }
72
+ actorRef.start();
73
+ return () => {
74
+ let canceled = false;
75
+ const cancel = () => {
76
+ canceled = true;
77
+ };
78
+ pendingStopsRef.current.set(actorRef, cancel);
79
+ queueMicrotask(() => {
80
+ if (!canceled) {
81
+ actorRef.stop();
82
+ }
83
+ if (pendingStopsRef.current.get(actorRef) === cancel) {
84
+ pendingStopsRef.current.delete(actorRef);
85
+ }
86
+ });
87
+ };
88
+ }, [actorRef]);
87
89
  }
88
90
  function useActorRef(machine, ...[options, observerOrListener]) {
89
- const actorRef = useIdleActorRef(machine, options);
91
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
90
92
  React.useEffect(() => {
91
93
  if (!observerOrListener) {
92
94
  return;
@@ -96,12 +98,7 @@ function useActorRef(machine, ...[options, observerOrListener]) {
96
98
  sub.unsubscribe();
97
99
  };
98
100
  }, [observerOrListener]);
99
- React.useEffect(() => {
100
- actorRef.start();
101
- return () => {
102
- stopRootWithRehydration(actorRef);
103
- };
104
- }, [actorRef]);
101
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(machine, actorRef.options));
105
102
  return actorRef;
106
103
  }
107
104
 
@@ -202,7 +199,7 @@ function useActor(logic, ...[options]) {
202
199
  if (!!logic && 'send' in logic && typeof logic.send === 'function') {
203
200
  throw new Error(`useActor() expects actor logic (e.g. a machine), but received an ActorRef. Use the useSelector(actorRef, ...) hook instead to read the ActorRef's snapshot.`);
204
201
  }
205
- const actorRef = useIdleActorRef(logic, options);
202
+ const [actorRef, setActorRef] = useIdleActorRef(logic, options);
206
203
  const getSnapshot = React.useCallback(() => {
207
204
  return actorRef.getSnapshot();
208
205
  }, [actorRef]);
@@ -220,12 +217,7 @@ function useActor(logic, ...[options]) {
220
217
  if (snapshotWithStatus?.status === 'error') {
221
218
  throw snapshotWithStatus.error;
222
219
  }
223
- React.useEffect(() => {
224
- actorRef.start();
225
- return () => {
226
- stopRootWithRehydration(actorRef);
227
- };
228
- }, [actorRef]);
220
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(logic, options));
229
221
  return [actorSnapshot, actorRef.send, actorRef];
230
222
  }
231
223
 
@@ -1,66 +1,68 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useState, useCallback } from 'react';
2
+ import { useEffect, useState, 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
- const forEachActor = (actorRef, callback) => {
9
- callback(actorRef);
10
- const children = actorRef.getSnapshot().children;
11
- if (children) {
12
- Object.values(children).forEach(child => {
13
- forEachActor(child, callback);
14
- });
15
- }
16
- };
17
- function stopRootWithRehydration(actorRef) {
18
- // persist snapshot here in a custom way allows us to persist inline actors and to preserve actor references
19
- // we do it to avoid setState in useEffect when the effect gets "reconnected"
20
- // this currently only happens in Strict Effects but it simulates the Offscreen aka Activity API
21
- // it also just allows us to end up with a somewhat more predictable behavior for the users
22
- const persistedSnapshots = [];
23
- forEachActor(actorRef, ref => {
24
- persistedSnapshots.push([ref, ref.getSnapshot()]);
25
- // muting observers allow us to avoid `useSelector` from being notified about the stopped snapshot
26
- // React reconnects its subscribers (from the useSyncExternalStore) on its own
27
- // and userland subscribers should basically always do the same anyway
28
- // as each subscription should have its own cleanup logic and that should be called each such reconnect
29
- ref.observers = new Set();
30
- });
31
- const systemSnapshot = actorRef.system.getSnapshot?.();
32
- actorRef.stop();
33
- actorRef.system._snapshot = systemSnapshot;
34
- persistedSnapshots.forEach(([ref, snapshot]) => {
35
- ref._processingStatus = 0;
36
- ref._snapshot = snapshot;
37
- });
38
- }
39
-
40
8
  function useIdleActorRef(logic, ...[options]) {
41
- let [[currentConfig, actorRef], setCurrent] = useState(() => {
42
- const actorRef = createActor(logic, options);
43
- return [logic.config, actorRef];
9
+ let [actorRef, setActorRef] = useState(() => {
10
+ return createActor(logic, options);
44
11
  });
45
- if (logic.config !== currentConfig) {
12
+ if (logic.config !== actorRef.logic.config) {
46
13
  const newActorRef = createActor(logic, {
47
14
  ...options,
48
15
  snapshot: actorRef.getPersistedSnapshot({
49
16
  __unsafeAllowInlineActors: true
50
17
  })
51
18
  });
52
- setCurrent([logic.config, newActorRef]);
19
+ setActorRef(newActorRef);
53
20
  actorRef = newActorRef;
54
21
  }
55
22
 
56
23
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
57
24
  useIsomorphicLayoutEffect(() => {
58
- actorRef.logic.implementations = logic.implementations;
25
+ actorRef.logic.sources = logic.sources;
59
26
  });
60
- return actorRef;
27
+ return [actorRef, setActorRef];
28
+ }
29
+ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
30
+ const pendingStopsRef = useRef(new Map());
31
+ useEffect(() => {
32
+ const cancelPendingStop = pendingStopsRef.current.get(actorRef);
33
+ if (cancelPendingStop) {
34
+ cancelPendingStop();
35
+ pendingStopsRef.current.delete(actorRef);
36
+ }
37
+
38
+ // If the actor was stopped before this effect reconnected, create a fresh
39
+ // actor. A stopped actor cannot be restarted.
40
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
41
+ const newActor = createReplacement();
42
+ newActor.start();
43
+ setActorRef(newActor);
44
+ return;
45
+ }
46
+ actorRef.start();
47
+ return () => {
48
+ let canceled = false;
49
+ const cancel = () => {
50
+ canceled = true;
51
+ };
52
+ pendingStopsRef.current.set(actorRef, cancel);
53
+ queueMicrotask(() => {
54
+ if (!canceled) {
55
+ actorRef.stop();
56
+ }
57
+ if (pendingStopsRef.current.get(actorRef) === cancel) {
58
+ pendingStopsRef.current.delete(actorRef);
59
+ }
60
+ });
61
+ };
62
+ }, [actorRef]);
61
63
  }
62
64
  function useActorRef(machine, ...[options, observerOrListener]) {
63
- const actorRef = useIdleActorRef(machine, options);
65
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
64
66
  useEffect(() => {
65
67
  if (!observerOrListener) {
66
68
  return;
@@ -70,12 +72,7 @@ function useActorRef(machine, ...[options, observerOrListener]) {
70
72
  sub.unsubscribe();
71
73
  };
72
74
  }, [observerOrListener]);
73
- useEffect(() => {
74
- actorRef.start();
75
- return () => {
76
- stopRootWithRehydration(actorRef);
77
- };
78
- }, [actorRef]);
75
+ useActorLifecycle(actorRef, setActorRef, () => createActor(machine, actorRef.options));
79
76
  return actorRef;
80
77
  }
81
78
 
@@ -176,7 +173,7 @@ function useActor(logic, ...[options]) {
176
173
  if (!!logic && 'send' in logic && typeof logic.send === 'function') {
177
174
  throw new Error(`useActor() expects actor logic (e.g. a machine), but received an ActorRef. Use the useSelector(actorRef, ...) hook instead to read the ActorRef's snapshot.`);
178
175
  }
179
- const actorRef = useIdleActorRef(logic, options);
176
+ const [actorRef, setActorRef] = useIdleActorRef(logic, options);
180
177
  const getSnapshot = useCallback(() => {
181
178
  return actorRef.getSnapshot();
182
179
  }, [actorRef]);
@@ -194,12 +191,7 @@ function useActor(logic, ...[options]) {
194
191
  if (snapshotWithStatus?.status === 'error') {
195
192
  throw snapshotWithStatus.error;
196
193
  }
197
- useEffect(() => {
198
- actorRef.start();
199
- return () => {
200
- stopRootWithRehydration(actorRef);
201
- };
202
- }, [actorRef]);
194
+ useActorLifecycle(actorRef, setActorRef, () => createActor(logic, options));
203
195
  return [actorSnapshot, actorRef.send, actorRef];
204
196
  }
205
197
 
@@ -1,66 +1,68 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useState, useCallback } from 'react';
2
+ import { useEffect, useState, 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
- const forEachActor = (actorRef, callback) => {
9
- callback(actorRef);
10
- const children = actorRef.getSnapshot().children;
11
- if (children) {
12
- Object.values(children).forEach(child => {
13
- forEachActor(child, callback);
14
- });
15
- }
16
- };
17
- function stopRootWithRehydration(actorRef) {
18
- // persist snapshot here in a custom way allows us to persist inline actors and to preserve actor references
19
- // we do it to avoid setState in useEffect when the effect gets "reconnected"
20
- // this currently only happens in Strict Effects but it simulates the Offscreen aka Activity API
21
- // it also just allows us to end up with a somewhat more predictable behavior for the users
22
- const persistedSnapshots = [];
23
- forEachActor(actorRef, ref => {
24
- persistedSnapshots.push([ref, ref.getSnapshot()]);
25
- // muting observers allow us to avoid `useSelector` from being notified about the stopped snapshot
26
- // React reconnects its subscribers (from the useSyncExternalStore) on its own
27
- // and userland subscribers should basically always do the same anyway
28
- // as each subscription should have its own cleanup logic and that should be called each such reconnect
29
- ref.observers = new Set();
30
- });
31
- const systemSnapshot = actorRef.system.getSnapshot?.();
32
- actorRef.stop();
33
- actorRef.system._snapshot = systemSnapshot;
34
- persistedSnapshots.forEach(([ref, snapshot]) => {
35
- ref._processingStatus = 0;
36
- ref._snapshot = snapshot;
37
- });
38
- }
39
-
40
8
  function useIdleActorRef(logic, ...[options]) {
41
- let [[currentConfig, actorRef], setCurrent] = useState(() => {
42
- const actorRef = createActor(logic, options);
43
- return [logic.config, actorRef];
9
+ let [actorRef, setActorRef] = useState(() => {
10
+ return createActor(logic, options);
44
11
  });
45
- if (logic.config !== currentConfig) {
12
+ if (logic.config !== actorRef.logic.config) {
46
13
  const newActorRef = createActor(logic, {
47
14
  ...options,
48
15
  snapshot: actorRef.getPersistedSnapshot({
49
16
  __unsafeAllowInlineActors: true
50
17
  })
51
18
  });
52
- setCurrent([logic.config, newActorRef]);
19
+ setActorRef(newActorRef);
53
20
  actorRef = newActorRef;
54
21
  }
55
22
 
56
23
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
57
24
  useIsomorphicLayoutEffect(() => {
58
- actorRef.logic.implementations = logic.implementations;
25
+ actorRef.logic.sources = logic.sources;
59
26
  });
60
- return actorRef;
27
+ return [actorRef, setActorRef];
28
+ }
29
+ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
30
+ const pendingStopsRef = useRef(new Map());
31
+ useEffect(() => {
32
+ const cancelPendingStop = pendingStopsRef.current.get(actorRef);
33
+ if (cancelPendingStop) {
34
+ cancelPendingStop();
35
+ pendingStopsRef.current.delete(actorRef);
36
+ }
37
+
38
+ // If the actor was stopped before this effect reconnected, create a fresh
39
+ // actor. A stopped actor cannot be restarted.
40
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
41
+ const newActor = createReplacement();
42
+ newActor.start();
43
+ setActorRef(newActor);
44
+ return;
45
+ }
46
+ actorRef.start();
47
+ return () => {
48
+ let canceled = false;
49
+ const cancel = () => {
50
+ canceled = true;
51
+ };
52
+ pendingStopsRef.current.set(actorRef, cancel);
53
+ queueMicrotask(() => {
54
+ if (!canceled) {
55
+ actorRef.stop();
56
+ }
57
+ if (pendingStopsRef.current.get(actorRef) === cancel) {
58
+ pendingStopsRef.current.delete(actorRef);
59
+ }
60
+ });
61
+ };
62
+ }, [actorRef]);
61
63
  }
62
64
  function useActorRef(machine, ...[options, observerOrListener]) {
63
- const actorRef = useIdleActorRef(machine, options);
65
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
64
66
  useEffect(() => {
65
67
  if (!observerOrListener) {
66
68
  return;
@@ -70,12 +72,7 @@ function useActorRef(machine, ...[options, observerOrListener]) {
70
72
  sub.unsubscribe();
71
73
  };
72
74
  }, [observerOrListener]);
73
- useEffect(() => {
74
- actorRef.start();
75
- return () => {
76
- stopRootWithRehydration(actorRef);
77
- };
78
- }, [actorRef]);
75
+ useActorLifecycle(actorRef, setActorRef, () => createActor(machine, actorRef.options));
79
76
  return actorRef;
80
77
  }
81
78
 
@@ -173,7 +170,7 @@ function shallowEqual(objA, objB) {
173
170
  }
174
171
 
175
172
  function useActor(logic, ...[options]) {
176
- const actorRef = useIdleActorRef(logic, options);
173
+ const [actorRef, setActorRef] = useIdleActorRef(logic, options);
177
174
  const getSnapshot = useCallback(() => {
178
175
  return actorRef.getSnapshot();
179
176
  }, [actorRef]);
@@ -191,12 +188,7 @@ function useActor(logic, ...[options]) {
191
188
  if (snapshotWithStatus?.status === 'error') {
192
189
  throw snapshotWithStatus.error;
193
190
  }
194
- useEffect(() => {
195
- actorRef.start();
196
- return () => {
197
- stopRootWithRehydration(actorRef);
198
- };
199
- }, [actorRef]);
191
+ useActorLifecycle(actorRef, setActorRef, () => createActor(logic, options));
200
192
  return [actorSnapshot, actorRef.send, actorRef];
201
193
  }
202
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xstate/react",
3
- "version": "6.1.0",
3
+ "version": "7.0.0-alpha.2",
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": "^5.28.0"
57
+ "xstate": "^6.0.0-alpha.52"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "xstate": {
@@ -73,7 +73,8 @@
73
73
  "react": "^19.0.0",
74
74
  "react-dom": "^19.0.0",
75
75
  "rxjs": "^7.8.1",
76
- "xstate": "^5.28.0"
76
+ "zod": "^3.25.51",
77
+ "xstate": "^6.0.0-alpha.52"
77
78
  },
78
79
  "scripts": {}
79
80
  }