@xstate/react 6.0.0 → 7.0.0-alpha.1

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.
@@ -3,7 +3,7 @@ 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
7
  export declare function useActorRef<TLogic extends AnyActorLogic>(machine: TLogic, ...[options, observerOrListener]: IsNotNever<RequiredActorOptionsKeys<TLogic>> extends true ? [
8
8
  options: ActorOptions<TLogic> & {
9
9
  [K in RequiredActorOptionsKeys<TLogic>]: unknown;
@@ -31,51 +31,18 @@ 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
 
@@ -83,10 +50,10 @@ function useIdleActorRef(logic, ...[options]) {
83
50
  useIsomorphicLayoutEffect__default["default"](() => {
84
51
  actorRef.logic.implementations = logic.implementations;
85
52
  });
86
- return actorRef;
53
+ return [actorRef, setActorRef];
87
54
  }
88
55
  function useActorRef(machine, ...[options, observerOrListener]) {
89
- const actorRef = useIdleActorRef(machine, options);
56
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
90
57
  React.useEffect(() => {
91
58
  if (!observerOrListener) {
92
59
  return;
@@ -97,9 +64,22 @@ function useActorRef(machine, ...[options, observerOrListener]) {
97
64
  };
98
65
  }, [observerOrListener]);
99
66
  React.useEffect(() => {
67
+ // If the actor was stopped by a previous cleanup (e.g. strict mode),
68
+ // create a fresh actor. The setActorRef triggers a re-render so
69
+ // useSyncExternalStore re-subscribes to the new actor.
70
+ // Only recreate if externally stopped ('stopped' status from XSTATE_STOP),
71
+ // not if it completed naturally ('done'/'error' status).
72
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
73
+ const newActor = xstate.createActor(machine, actorRef.options);
74
+ newActor.start();
75
+ setActorRef(newActor);
76
+ // No cleanup — the re-render will run this effect again with the
77
+ // new actorRef (which is Running), registering the stop cleanup then.
78
+ return;
79
+ }
100
80
  actorRef.start();
101
81
  return () => {
102
- stopRootWithRehydration(actorRef);
82
+ actorRef.stop();
103
83
  };
104
84
  }, [actorRef]);
105
85
  return actorRef;
@@ -115,10 +95,19 @@ function useSelector(actor, selector, compare = defaultCompare) {
115
95
  }
116
96
  const {
117
97
  unsubscribe
118
- } = actor.subscribe(handleStoreChange);
98
+ } = actor.subscribe({
99
+ next: handleStoreChange,
100
+ error: handleStoreChange
101
+ });
119
102
  return unsubscribe;
120
103
  }, [actor]);
121
- const boundGetSnapshot = React.useCallback(() => actor?.getSnapshot(), [actor]);
104
+ const boundGetSnapshot = React.useCallback(() => {
105
+ const snapshot = actor?.getSnapshot();
106
+ if (snapshot && 'status' in snapshot && snapshot.status === 'error') {
107
+ throw snapshot.error;
108
+ }
109
+ return snapshot;
110
+ }, [actor]);
122
111
  const selectedSnapshot = withSelector.useSyncExternalStoreWithSelector(subscribe, boundGetSnapshot, boundGetSnapshot, selector, compare);
123
112
  return selectedSnapshot;
124
113
  }
@@ -190,21 +179,34 @@ function shallowEqual(objA, objB) {
190
179
  }
191
180
 
192
181
  function useActor(logic, ...[options]) {
193
- const actorRef = useIdleActorRef(logic, options);
182
+ const [actorRef, setActorRef] = useIdleActorRef(logic, options);
194
183
  const getSnapshot = React.useCallback(() => {
195
184
  return actorRef.getSnapshot();
196
185
  }, [actorRef]);
197
186
  const subscribe = React.useCallback(handleStoreChange => {
198
187
  const {
199
188
  unsubscribe
200
- } = actorRef.subscribe(handleStoreChange);
189
+ } = actorRef.subscribe({
190
+ next: handleStoreChange,
191
+ error: handleStoreChange
192
+ });
201
193
  return unsubscribe;
202
194
  }, [actorRef]);
203
195
  const actorSnapshot = shim.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
196
+ const snapshotWithStatus = 'status' in actorSnapshot ? actorSnapshot : undefined;
197
+ if (snapshotWithStatus?.status === 'error') {
198
+ throw snapshotWithStatus.error;
199
+ }
204
200
  React.useEffect(() => {
201
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
202
+ const newActor = xstate.createActor(logic, options);
203
+ newActor.start();
204
+ setActorRef(newActor);
205
+ return;
206
+ }
205
207
  actorRef.start();
206
208
  return () => {
207
- stopRootWithRehydration(actorRef);
209
+ actorRef.stop();
208
210
  };
209
211
  }, [actorRef]);
210
212
  return [actorSnapshot, actorRef.send, actorRef];
@@ -31,51 +31,18 @@ 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
 
@@ -83,10 +50,10 @@ function useIdleActorRef(logic, ...[options]) {
83
50
  useIsomorphicLayoutEffect__default["default"](() => {
84
51
  actorRef.logic.implementations = logic.implementations;
85
52
  });
86
- return actorRef;
53
+ return [actorRef, setActorRef];
87
54
  }
88
55
  function useActorRef(machine, ...[options, observerOrListener]) {
89
- const actorRef = useIdleActorRef(machine, options);
56
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
90
57
  React.useEffect(() => {
91
58
  if (!observerOrListener) {
92
59
  return;
@@ -97,9 +64,22 @@ function useActorRef(machine, ...[options, observerOrListener]) {
97
64
  };
98
65
  }, [observerOrListener]);
99
66
  React.useEffect(() => {
67
+ // If the actor was stopped by a previous cleanup (e.g. strict mode),
68
+ // create a fresh actor. The setActorRef triggers a re-render so
69
+ // useSyncExternalStore re-subscribes to the new actor.
70
+ // Only recreate if externally stopped ('stopped' status from XSTATE_STOP),
71
+ // not if it completed naturally ('done'/'error' status).
72
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
73
+ const newActor = xstate.createActor(machine, actorRef.options);
74
+ newActor.start();
75
+ setActorRef(newActor);
76
+ // No cleanup — the re-render will run this effect again with the
77
+ // new actorRef (which is Running), registering the stop cleanup then.
78
+ return;
79
+ }
100
80
  actorRef.start();
101
81
  return () => {
102
- stopRootWithRehydration(actorRef);
82
+ actorRef.stop();
103
83
  };
104
84
  }, [actorRef]);
105
85
  return actorRef;
@@ -115,10 +95,19 @@ function useSelector(actor, selector, compare = defaultCompare) {
115
95
  }
116
96
  const {
117
97
  unsubscribe
118
- } = actor.subscribe(handleStoreChange);
98
+ } = actor.subscribe({
99
+ next: handleStoreChange,
100
+ error: handleStoreChange
101
+ });
119
102
  return unsubscribe;
120
103
  }, [actor]);
121
- const boundGetSnapshot = React.useCallback(() => actor?.getSnapshot(), [actor]);
104
+ const boundGetSnapshot = React.useCallback(() => {
105
+ const snapshot = actor?.getSnapshot();
106
+ if (snapshot && 'status' in snapshot && snapshot.status === 'error') {
107
+ throw snapshot.error;
108
+ }
109
+ return snapshot;
110
+ }, [actor]);
122
111
  const selectedSnapshot = withSelector.useSyncExternalStoreWithSelector(subscribe, boundGetSnapshot, boundGetSnapshot, selector, compare);
123
112
  return selectedSnapshot;
124
113
  }
@@ -193,21 +182,34 @@ function useActor(logic, ...[options]) {
193
182
  if (!!logic && 'send' in logic && typeof logic.send === 'function') {
194
183
  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.`);
195
184
  }
196
- const actorRef = useIdleActorRef(logic, options);
185
+ const [actorRef, setActorRef] = useIdleActorRef(logic, options);
197
186
  const getSnapshot = React.useCallback(() => {
198
187
  return actorRef.getSnapshot();
199
188
  }, [actorRef]);
200
189
  const subscribe = React.useCallback(handleStoreChange => {
201
190
  const {
202
191
  unsubscribe
203
- } = actorRef.subscribe(handleStoreChange);
192
+ } = actorRef.subscribe({
193
+ next: handleStoreChange,
194
+ error: handleStoreChange
195
+ });
204
196
  return unsubscribe;
205
197
  }, [actorRef]);
206
198
  const actorSnapshot = shim.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
199
+ const snapshotWithStatus = 'status' in actorSnapshot ? actorSnapshot : undefined;
200
+ if (snapshotWithStatus?.status === 'error') {
201
+ throw snapshotWithStatus.error;
202
+ }
207
203
  React.useEffect(() => {
204
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
205
+ const newActor = xstate.createActor(logic, options);
206
+ newActor.start();
207
+ setActorRef(newActor);
208
+ return;
209
+ }
208
210
  actorRef.start();
209
211
  return () => {
210
- stopRootWithRehydration(actorRef);
212
+ actorRef.stop();
211
213
  };
212
214
  }, [actorRef]);
213
215
  return [actorSnapshot, actorRef.send, actorRef];
@@ -5,51 +5,18 @@ 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
 
@@ -57,10 +24,10 @@ function useIdleActorRef(logic, ...[options]) {
57
24
  useIsomorphicLayoutEffect(() => {
58
25
  actorRef.logic.implementations = logic.implementations;
59
26
  });
60
- return actorRef;
27
+ return [actorRef, setActorRef];
61
28
  }
62
29
  function useActorRef(machine, ...[options, observerOrListener]) {
63
- const actorRef = useIdleActorRef(machine, options);
30
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
64
31
  useEffect(() => {
65
32
  if (!observerOrListener) {
66
33
  return;
@@ -71,9 +38,22 @@ function useActorRef(machine, ...[options, observerOrListener]) {
71
38
  };
72
39
  }, [observerOrListener]);
73
40
  useEffect(() => {
41
+ // If the actor was stopped by a previous cleanup (e.g. strict mode),
42
+ // create a fresh actor. The setActorRef triggers a re-render so
43
+ // useSyncExternalStore re-subscribes to the new actor.
44
+ // Only recreate if externally stopped ('stopped' status from XSTATE_STOP),
45
+ // not if it completed naturally ('done'/'error' status).
46
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
47
+ const newActor = createActor(machine, actorRef.options);
48
+ newActor.start();
49
+ setActorRef(newActor);
50
+ // No cleanup — the re-render will run this effect again with the
51
+ // new actorRef (which is Running), registering the stop cleanup then.
52
+ return;
53
+ }
74
54
  actorRef.start();
75
55
  return () => {
76
- stopRootWithRehydration(actorRef);
56
+ actorRef.stop();
77
57
  };
78
58
  }, [actorRef]);
79
59
  return actorRef;
@@ -89,10 +69,19 @@ function useSelector(actor, selector, compare = defaultCompare) {
89
69
  }
90
70
  const {
91
71
  unsubscribe
92
- } = actor.subscribe(handleStoreChange);
72
+ } = actor.subscribe({
73
+ next: handleStoreChange,
74
+ error: handleStoreChange
75
+ });
93
76
  return unsubscribe;
94
77
  }, [actor]);
95
- const boundGetSnapshot = useCallback(() => actor?.getSnapshot(), [actor]);
78
+ const boundGetSnapshot = useCallback(() => {
79
+ const snapshot = actor?.getSnapshot();
80
+ if (snapshot && 'status' in snapshot && snapshot.status === 'error') {
81
+ throw snapshot.error;
82
+ }
83
+ return snapshot;
84
+ }, [actor]);
96
85
  const selectedSnapshot = useSyncExternalStoreWithSelector(subscribe, boundGetSnapshot, boundGetSnapshot, selector, compare);
97
86
  return selectedSnapshot;
98
87
  }
@@ -167,21 +156,34 @@ function useActor(logic, ...[options]) {
167
156
  if (!!logic && 'send' in logic && typeof logic.send === 'function') {
168
157
  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.`);
169
158
  }
170
- const actorRef = useIdleActorRef(logic, options);
159
+ const [actorRef, setActorRef] = useIdleActorRef(logic, options);
171
160
  const getSnapshot = useCallback(() => {
172
161
  return actorRef.getSnapshot();
173
162
  }, [actorRef]);
174
163
  const subscribe = useCallback(handleStoreChange => {
175
164
  const {
176
165
  unsubscribe
177
- } = actorRef.subscribe(handleStoreChange);
166
+ } = actorRef.subscribe({
167
+ next: handleStoreChange,
168
+ error: handleStoreChange
169
+ });
178
170
  return unsubscribe;
179
171
  }, [actorRef]);
180
172
  const actorSnapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
173
+ const snapshotWithStatus = 'status' in actorSnapshot ? actorSnapshot : undefined;
174
+ if (snapshotWithStatus?.status === 'error') {
175
+ throw snapshotWithStatus.error;
176
+ }
181
177
  useEffect(() => {
178
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
179
+ const newActor = createActor(logic, options);
180
+ newActor.start();
181
+ setActorRef(newActor);
182
+ return;
183
+ }
182
184
  actorRef.start();
183
185
  return () => {
184
- stopRootWithRehydration(actorRef);
186
+ actorRef.stop();
185
187
  };
186
188
  }, [actorRef]);
187
189
  return [actorSnapshot, actorRef.send, actorRef];
@@ -5,51 +5,18 @@ 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
 
@@ -57,10 +24,10 @@ function useIdleActorRef(logic, ...[options]) {
57
24
  useIsomorphicLayoutEffect(() => {
58
25
  actorRef.logic.implementations = logic.implementations;
59
26
  });
60
- return actorRef;
27
+ return [actorRef, setActorRef];
61
28
  }
62
29
  function useActorRef(machine, ...[options, observerOrListener]) {
63
- const actorRef = useIdleActorRef(machine, options);
30
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
64
31
  useEffect(() => {
65
32
  if (!observerOrListener) {
66
33
  return;
@@ -71,9 +38,22 @@ function useActorRef(machine, ...[options, observerOrListener]) {
71
38
  };
72
39
  }, [observerOrListener]);
73
40
  useEffect(() => {
41
+ // If the actor was stopped by a previous cleanup (e.g. strict mode),
42
+ // create a fresh actor. The setActorRef triggers a re-render so
43
+ // useSyncExternalStore re-subscribes to the new actor.
44
+ // Only recreate if externally stopped ('stopped' status from XSTATE_STOP),
45
+ // not if it completed naturally ('done'/'error' status).
46
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
47
+ const newActor = createActor(machine, actorRef.options);
48
+ newActor.start();
49
+ setActorRef(newActor);
50
+ // No cleanup — the re-render will run this effect again with the
51
+ // new actorRef (which is Running), registering the stop cleanup then.
52
+ return;
53
+ }
74
54
  actorRef.start();
75
55
  return () => {
76
- stopRootWithRehydration(actorRef);
56
+ actorRef.stop();
77
57
  };
78
58
  }, [actorRef]);
79
59
  return actorRef;
@@ -89,10 +69,19 @@ function useSelector(actor, selector, compare = defaultCompare) {
89
69
  }
90
70
  const {
91
71
  unsubscribe
92
- } = actor.subscribe(handleStoreChange);
72
+ } = actor.subscribe({
73
+ next: handleStoreChange,
74
+ error: handleStoreChange
75
+ });
93
76
  return unsubscribe;
94
77
  }, [actor]);
95
- const boundGetSnapshot = useCallback(() => actor?.getSnapshot(), [actor]);
78
+ const boundGetSnapshot = useCallback(() => {
79
+ const snapshot = actor?.getSnapshot();
80
+ if (snapshot && 'status' in snapshot && snapshot.status === 'error') {
81
+ throw snapshot.error;
82
+ }
83
+ return snapshot;
84
+ }, [actor]);
96
85
  const selectedSnapshot = useSyncExternalStoreWithSelector(subscribe, boundGetSnapshot, boundGetSnapshot, selector, compare);
97
86
  return selectedSnapshot;
98
87
  }
@@ -164,21 +153,34 @@ function shallowEqual(objA, objB) {
164
153
  }
165
154
 
166
155
  function useActor(logic, ...[options]) {
167
- const actorRef = useIdleActorRef(logic, options);
156
+ const [actorRef, setActorRef] = useIdleActorRef(logic, options);
168
157
  const getSnapshot = useCallback(() => {
169
158
  return actorRef.getSnapshot();
170
159
  }, [actorRef]);
171
160
  const subscribe = useCallback(handleStoreChange => {
172
161
  const {
173
162
  unsubscribe
174
- } = actorRef.subscribe(handleStoreChange);
163
+ } = actorRef.subscribe({
164
+ next: handleStoreChange,
165
+ error: handleStoreChange
166
+ });
175
167
  return unsubscribe;
176
168
  }, [actorRef]);
177
169
  const actorSnapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
170
+ const snapshotWithStatus = 'status' in actorSnapshot ? actorSnapshot : undefined;
171
+ if (snapshotWithStatus?.status === 'error') {
172
+ throw snapshotWithStatus.error;
173
+ }
178
174
  useEffect(() => {
175
+ if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
176
+ const newActor = createActor(logic, options);
177
+ newActor.start();
178
+ setActorRef(newActor);
179
+ return;
180
+ }
179
181
  actorRef.start();
180
182
  return () => {
181
- stopRootWithRehydration(actorRef);
183
+ actorRef.stop();
182
184
  };
183
185
  }, [actorRef]);
184
186
  return [actorSnapshot, actorRef.send, actorRef];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xstate/react",
3
- "version": "6.0.0",
3
+ "version": "7.0.0-alpha.1",
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.20.0"
57
+ "xstate": "^6.0.0-alpha.1"
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.20.0"
76
+ "zod": "^3.25.51",
77
+ "xstate": "^6.0.0-alpha.1"
77
78
  },
78
79
  "scripts": {}
79
80
  }