@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;
|
package/dist/xstate-react.cjs.js
CHANGED
|
@@ -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 [
|
|
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
|
|
|
@@ -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
|
-
|
|
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(
|
|
98
|
+
} = actor.subscribe({
|
|
99
|
+
next: handleStoreChange,
|
|
100
|
+
error: handleStoreChange
|
|
101
|
+
});
|
|
119
102
|
return unsubscribe;
|
|
120
103
|
}, [actor]);
|
|
121
|
-
const boundGetSnapshot = React.useCallback(() =>
|
|
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(
|
|
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
|
-
|
|
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 [
|
|
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
|
|
|
@@ -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
|
-
|
|
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(
|
|
98
|
+
} = actor.subscribe({
|
|
99
|
+
next: handleStoreChange,
|
|
100
|
+
error: handleStoreChange
|
|
101
|
+
});
|
|
119
102
|
return unsubscribe;
|
|
120
103
|
}, [actor]);
|
|
121
|
-
const boundGetSnapshot = React.useCallback(() =>
|
|
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(
|
|
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
|
-
|
|
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 [
|
|
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
|
|
|
@@ -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
|
-
|
|
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(
|
|
72
|
+
} = actor.subscribe({
|
|
73
|
+
next: handleStoreChange,
|
|
74
|
+
error: handleStoreChange
|
|
75
|
+
});
|
|
93
76
|
return unsubscribe;
|
|
94
77
|
}, [actor]);
|
|
95
|
-
const boundGetSnapshot = useCallback(() =>
|
|
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(
|
|
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
|
-
|
|
186
|
+
actorRef.stop();
|
|
185
187
|
};
|
|
186
188
|
}, [actorRef]);
|
|
187
189
|
return [actorSnapshot, actorRef.send, actorRef];
|
package/dist/xstate-react.esm.js
CHANGED
|
@@ -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 [
|
|
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
|
|
|
@@ -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
|
-
|
|
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(
|
|
72
|
+
} = actor.subscribe({
|
|
73
|
+
next: handleStoreChange,
|
|
74
|
+
error: handleStoreChange
|
|
75
|
+
});
|
|
93
76
|
return unsubscribe;
|
|
94
77
|
}, [actor]);
|
|
95
|
-
const boundGetSnapshot = useCallback(() =>
|
|
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(
|
|
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
|
-
|
|
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": "
|
|
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": "^
|
|
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
|
-
"
|
|
76
|
+
"zod": "^3.25.51",
|
|
77
|
+
"xstate": "^6.0.0-alpha.1"
|
|
77
78
|
},
|
|
78
79
|
"scripts": {}
|
|
79
80
|
}
|