@xstate/react 7.0.0-alpha.1 → 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
  });
@@ -4,6 +4,7 @@ export declare function useIdleActorRef<TLogic extends AnyActorLogic>(logic: TLo
4
4
  [K in RequiredActorOptionsKeys<TLogic>]: unknown;
5
5
  }
6
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;
@@ -48,40 +48,57 @@ function useIdleActorRef(logic, ...[options]) {
48
48
 
49
49
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
50
50
  useIsomorphicLayoutEffect__default["default"](() => {
51
- actorRef.logic.implementations = logic.implementations;
51
+ actorRef.logic.sources = logic.sources;
52
52
  });
53
53
  return [actorRef, setActorRef];
54
54
  }
55
- function useActorRef(machine, ...[options, observerOrListener]) {
56
- const [actorRef, setActorRef] = useIdleActorRef(machine, options);
55
+ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
56
+ const pendingStopsRef = React.useRef(new Map());
57
57
  React.useEffect(() => {
58
- if (!observerOrListener) {
59
- return;
58
+ const cancelPendingStop = pendingStopsRef.current.get(actorRef);
59
+ if (cancelPendingStop) {
60
+ cancelPendingStop();
61
+ pendingStopsRef.current.delete(actorRef);
60
62
  }
61
- const sub = actorRef.subscribe(xstate.toObserver(observerOrListener));
62
- return () => {
63
- sub.unsubscribe();
64
- };
65
- }, [observerOrListener]);
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).
63
+
64
+ // If the actor was stopped before this effect reconnected, create a fresh
65
+ // actor. A stopped actor cannot be restarted.
72
66
  if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
73
- const newActor = xstate.createActor(machine, actorRef.options);
67
+ const newActor = createReplacement();
74
68
  newActor.start();
75
69
  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
70
  return;
79
71
  }
80
72
  actorRef.start();
81
73
  return () => {
82
- actorRef.stop();
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
+ });
83
87
  };
84
88
  }, [actorRef]);
89
+ }
90
+ function useActorRef(machine, ...[options, observerOrListener]) {
91
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
92
+ React.useEffect(() => {
93
+ if (!observerOrListener) {
94
+ return;
95
+ }
96
+ const sub = actorRef.subscribe(xstate.toObserver(observerOrListener));
97
+ return () => {
98
+ sub.unsubscribe();
99
+ };
100
+ }, [observerOrListener]);
101
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(machine, actorRef.options));
85
102
  return actorRef;
86
103
  }
87
104
 
@@ -197,18 +214,7 @@ function useActor(logic, ...[options]) {
197
214
  if (snapshotWithStatus?.status === 'error') {
198
215
  throw snapshotWithStatus.error;
199
216
  }
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
- }
207
- actorRef.start();
208
- return () => {
209
- actorRef.stop();
210
- };
211
- }, [actorRef]);
217
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(logic, options));
212
218
  return [actorSnapshot, actorRef.send, actorRef];
213
219
  }
214
220
 
@@ -48,40 +48,57 @@ function useIdleActorRef(logic, ...[options]) {
48
48
 
49
49
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
50
50
  useIsomorphicLayoutEffect__default["default"](() => {
51
- actorRef.logic.implementations = logic.implementations;
51
+ actorRef.logic.sources = logic.sources;
52
52
  });
53
53
  return [actorRef, setActorRef];
54
54
  }
55
- function useActorRef(machine, ...[options, observerOrListener]) {
56
- const [actorRef, setActorRef] = useIdleActorRef(machine, options);
55
+ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
56
+ const pendingStopsRef = React.useRef(new Map());
57
57
  React.useEffect(() => {
58
- if (!observerOrListener) {
59
- return;
58
+ const cancelPendingStop = pendingStopsRef.current.get(actorRef);
59
+ if (cancelPendingStop) {
60
+ cancelPendingStop();
61
+ pendingStopsRef.current.delete(actorRef);
60
62
  }
61
- const sub = actorRef.subscribe(xstate.toObserver(observerOrListener));
62
- return () => {
63
- sub.unsubscribe();
64
- };
65
- }, [observerOrListener]);
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).
63
+
64
+ // If the actor was stopped before this effect reconnected, create a fresh
65
+ // actor. A stopped actor cannot be restarted.
72
66
  if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
73
- const newActor = xstate.createActor(machine, actorRef.options);
67
+ const newActor = createReplacement();
74
68
  newActor.start();
75
69
  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
70
  return;
79
71
  }
80
72
  actorRef.start();
81
73
  return () => {
82
- actorRef.stop();
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
+ });
83
87
  };
84
88
  }, [actorRef]);
89
+ }
90
+ function useActorRef(machine, ...[options, observerOrListener]) {
91
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
92
+ React.useEffect(() => {
93
+ if (!observerOrListener) {
94
+ return;
95
+ }
96
+ const sub = actorRef.subscribe(xstate.toObserver(observerOrListener));
97
+ return () => {
98
+ sub.unsubscribe();
99
+ };
100
+ }, [observerOrListener]);
101
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(machine, actorRef.options));
85
102
  return actorRef;
86
103
  }
87
104
 
@@ -200,18 +217,7 @@ function useActor(logic, ...[options]) {
200
217
  if (snapshotWithStatus?.status === 'error') {
201
218
  throw snapshotWithStatus.error;
202
219
  }
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
- }
210
- actorRef.start();
211
- return () => {
212
- actorRef.stop();
213
- };
214
- }, [actorRef]);
220
+ useActorLifecycle(actorRef, setActorRef, () => xstate.createActor(logic, options));
215
221
  return [actorSnapshot, actorRef.send, actorRef];
216
222
  }
217
223
 
@@ -1,5 +1,5 @@
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';
@@ -22,40 +22,57 @@ function useIdleActorRef(logic, ...[options]) {
22
22
 
23
23
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
24
24
  useIsomorphicLayoutEffect(() => {
25
- actorRef.logic.implementations = logic.implementations;
25
+ actorRef.logic.sources = logic.sources;
26
26
  });
27
27
  return [actorRef, setActorRef];
28
28
  }
29
- function useActorRef(machine, ...[options, observerOrListener]) {
30
- const [actorRef, setActorRef] = useIdleActorRef(machine, options);
29
+ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
30
+ const pendingStopsRef = useRef(new Map());
31
31
  useEffect(() => {
32
- if (!observerOrListener) {
33
- return;
32
+ const cancelPendingStop = pendingStopsRef.current.get(actorRef);
33
+ if (cancelPendingStop) {
34
+ cancelPendingStop();
35
+ pendingStopsRef.current.delete(actorRef);
34
36
  }
35
- const sub = actorRef.subscribe(toObserver(observerOrListener));
36
- return () => {
37
- sub.unsubscribe();
38
- };
39
- }, [observerOrListener]);
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).
37
+
38
+ // If the actor was stopped before this effect reconnected, create a fresh
39
+ // actor. A stopped actor cannot be restarted.
46
40
  if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
47
- const newActor = createActor(machine, actorRef.options);
41
+ const newActor = createReplacement();
48
42
  newActor.start();
49
43
  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
44
  return;
53
45
  }
54
46
  actorRef.start();
55
47
  return () => {
56
- actorRef.stop();
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
+ });
57
61
  };
58
62
  }, [actorRef]);
63
+ }
64
+ function useActorRef(machine, ...[options, observerOrListener]) {
65
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
66
+ useEffect(() => {
67
+ if (!observerOrListener) {
68
+ return;
69
+ }
70
+ const sub = actorRef.subscribe(toObserver(observerOrListener));
71
+ return () => {
72
+ sub.unsubscribe();
73
+ };
74
+ }, [observerOrListener]);
75
+ useActorLifecycle(actorRef, setActorRef, () => createActor(machine, actorRef.options));
59
76
  return actorRef;
60
77
  }
61
78
 
@@ -174,18 +191,7 @@ function useActor(logic, ...[options]) {
174
191
  if (snapshotWithStatus?.status === 'error') {
175
192
  throw snapshotWithStatus.error;
176
193
  }
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
- }
184
- actorRef.start();
185
- return () => {
186
- actorRef.stop();
187
- };
188
- }, [actorRef]);
194
+ useActorLifecycle(actorRef, setActorRef, () => createActor(logic, options));
189
195
  return [actorSnapshot, actorRef.send, actorRef];
190
196
  }
191
197
 
@@ -1,5 +1,5 @@
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';
@@ -22,40 +22,57 @@ function useIdleActorRef(logic, ...[options]) {
22
22
 
23
23
  // TODO: consider using `useAsapEffect` that would do this in `useInsertionEffect` is that's available
24
24
  useIsomorphicLayoutEffect(() => {
25
- actorRef.logic.implementations = logic.implementations;
25
+ actorRef.logic.sources = logic.sources;
26
26
  });
27
27
  return [actorRef, setActorRef];
28
28
  }
29
- function useActorRef(machine, ...[options, observerOrListener]) {
30
- const [actorRef, setActorRef] = useIdleActorRef(machine, options);
29
+ function useActorLifecycle(actorRef, setActorRef, createReplacement) {
30
+ const pendingStopsRef = useRef(new Map());
31
31
  useEffect(() => {
32
- if (!observerOrListener) {
33
- return;
32
+ const cancelPendingStop = pendingStopsRef.current.get(actorRef);
33
+ if (cancelPendingStop) {
34
+ cancelPendingStop();
35
+ pendingStopsRef.current.delete(actorRef);
34
36
  }
35
- const sub = actorRef.subscribe(toObserver(observerOrListener));
36
- return () => {
37
- sub.unsubscribe();
38
- };
39
- }, [observerOrListener]);
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).
37
+
38
+ // If the actor was stopped before this effect reconnected, create a fresh
39
+ // actor. A stopped actor cannot be restarted.
46
40
  if (actorRef._processingStatus === 2 /* ProcessingStatus.Stopped */ && actorRef.getSnapshot()?.status === 'stopped') {
47
- const newActor = createActor(machine, actorRef.options);
41
+ const newActor = createReplacement();
48
42
  newActor.start();
49
43
  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
44
  return;
53
45
  }
54
46
  actorRef.start();
55
47
  return () => {
56
- actorRef.stop();
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
+ });
57
61
  };
58
62
  }, [actorRef]);
63
+ }
64
+ function useActorRef(machine, ...[options, observerOrListener]) {
65
+ const [actorRef, setActorRef] = useIdleActorRef(machine, options);
66
+ useEffect(() => {
67
+ if (!observerOrListener) {
68
+ return;
69
+ }
70
+ const sub = actorRef.subscribe(toObserver(observerOrListener));
71
+ return () => {
72
+ sub.unsubscribe();
73
+ };
74
+ }, [observerOrListener]);
75
+ useActorLifecycle(actorRef, setActorRef, () => createActor(machine, actorRef.options));
59
76
  return actorRef;
60
77
  }
61
78
 
@@ -171,18 +188,7 @@ function useActor(logic, ...[options]) {
171
188
  if (snapshotWithStatus?.status === 'error') {
172
189
  throw snapshotWithStatus.error;
173
190
  }
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
- }
181
- actorRef.start();
182
- return () => {
183
- actorRef.stop();
184
- };
185
- }, [actorRef]);
191
+ useActorLifecycle(actorRef, setActorRef, () => createActor(logic, options));
186
192
  return [actorSnapshot, actorRef.send, actorRef];
187
193
  }
188
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xstate/react",
3
- "version": "7.0.0-alpha.1",
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": "^6.0.0-alpha.1"
57
+ "xstate": "^6.0.0-alpha.52"
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.1"
77
+ "xstate": "^6.0.0-alpha.52"
78
78
  },
79
79
  "scripts": {}
80
80
  }