@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;
|
package/dist/xstate-react.cjs.js
CHANGED
|
@@ -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 [
|
|
68
|
-
|
|
69
|
-
return [logic.config, actorRef];
|
|
35
|
+
let [actorRef, setActorRef] = React.useState(() => {
|
|
36
|
+
return xstate.createActor(logic, options);
|
|
70
37
|
});
|
|
71
|
-
if (logic.config !==
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
68
|
-
|
|
69
|
-
return [logic.config, actorRef];
|
|
35
|
+
let [actorRef, setActorRef] = React.useState(() => {
|
|
36
|
+
return xstate.createActor(logic, options);
|
|
70
37
|
});
|
|
71
|
-
if (logic.config !==
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
42
|
-
|
|
43
|
-
return [logic.config, actorRef];
|
|
9
|
+
let [actorRef, setActorRef] = useState(() => {
|
|
10
|
+
return createActor(logic, options);
|
|
44
11
|
});
|
|
45
|
-
if (logic.config !==
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/xstate-react.esm.js
CHANGED
|
@@ -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 [
|
|
42
|
-
|
|
43
|
-
return [logic.config, actorRef];
|
|
9
|
+
let [actorRef, setActorRef] = useState(() => {
|
|
10
|
+
return createActor(logic, options);
|
|
44
11
|
});
|
|
45
|
-
if (logic.config !==
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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": "^
|
|
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
|
-
"
|
|
76
|
+
"zod": "^3.25.51",
|
|
77
|
+
"xstate": "^6.0.0-alpha.52"
|
|
77
78
|
},
|
|
78
79
|
"scripts": {}
|
|
79
80
|
}
|