Skip to content

Commit 6d515d0

Browse files
committed
Throw errors from actor snapshots to trigger error boundaries in React
1 parent b59a393 commit 6d515d0

3 files changed

Lines changed: 48 additions & 9 deletions

File tree

packages/xstate-react/src/useActor.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import isDevelopment from '#is-development';
22
import { useCallback, useEffect } from 'react';
33
import { useSyncExternalStore } from 'use-sync-external-store/shim';
4-
import { Actor, ActorOptions, AnyActorLogic, SnapshotFrom } from 'xstate';
4+
import {
5+
Actor,
6+
ActorOptions,
7+
AnyActorLogic,
8+
Snapshot,
9+
SnapshotFrom
10+
} from 'xstate';
511
import { stopRootWithRehydration } from './stopRootWithRehydration.ts';
612
import { useIdleActorRef } from './useActorRef.ts';
713

@@ -28,7 +34,10 @@ export function useActor<TLogic extends AnyActorLogic>(
2834

2935
const subscribe = useCallback(
3036
(handleStoreChange) => {
31-
const { unsubscribe } = actorRef.subscribe(handleStoreChange);
37+
const { unsubscribe } = actorRef.subscribe(
38+
handleStoreChange,
39+
handleStoreChange
40+
);
3241
return unsubscribe;
3342
},
3443
[actorRef]
@@ -40,6 +49,10 @@ export function useActor<TLogic extends AnyActorLogic>(
4049
getSnapshot
4150
);
4251

52+
if ((actorSnapshot as Snapshot<any>).status === 'error') {
53+
throw (actorSnapshot as Snapshot<any>).error;
54+
}
55+
4356
useEffect(() => {
4457
actorRef.start();
4558

packages/xstate-react/src/useActorRef.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
AnyActorLogic,
77
AnyStateMachine,
88
Observer,
9+
Snapshot,
910
SnapshotFrom,
1011
createActor,
1112
toObserver
@@ -42,6 +43,8 @@ export function useIdleActorRef<TLogic extends AnyActorLogic>(
4243
return actorRef;
4344
}
4445

46+
const UNIQUE = {};
47+
4548
export function useActorRef<TLogic extends AnyActorLogic>(
4649
machine: TLogic,
4750
options: ActorOptions<TLogic> = {},
@@ -50,12 +53,23 @@ export function useActorRef<TLogic extends AnyActorLogic>(
5053
| ((value: SnapshotFrom<TLogic>) => void)
5154
): Actor<TLogic> {
5255
const actorRef = useIdleActorRef(machine, options);
56+
const [reactError, setReactError] = useState(() => {
57+
const initialSnapshot: Snapshot<any> = actorRef.getSnapshot();
58+
return initialSnapshot.status === 'error' ? initialSnapshot.error : UNIQUE;
59+
});
60+
61+
if (reactError !== UNIQUE) {
62+
throw reactError;
63+
}
5364

5465
useEffect(() => {
55-
if (!observerOrListener) {
56-
return;
57-
}
58-
let sub = actorRef.subscribe(toObserver(observerOrListener));
66+
const observer = toObserver(observerOrListener);
67+
const errorListener = observer.error;
68+
observer.error = (error) => {
69+
setReactError(error);
70+
errorListener?.(error);
71+
};
72+
let sub = actorRef.subscribe(observer);
5973
return () => {
6074
sub.unsubscribe();
6175
};

packages/xstate-react/src/useSelector.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCallback } from 'react';
22
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
3-
import { ActorRef, SnapshotFrom } from 'xstate';
3+
import { ActorRef, Snapshot, SnapshotFrom } from 'xstate';
44

55
function defaultCompare<T>(a: T, b: T) {
66
return a === b;
@@ -13,19 +13,31 @@ export function useSelector<TActor extends ActorRef<any, any>, T>(
1313
): T {
1414
const subscribe = useCallback(
1515
(handleStoreChange) => {
16-
const { unsubscribe } = actor.subscribe(handleStoreChange);
16+
const { unsubscribe } = actor.subscribe(
17+
handleStoreChange,
18+
handleStoreChange
19+
);
1720
return unsubscribe;
1821
},
1922
[actor]
2023
);
2124

2225
const boundGetSnapshot = useCallback(() => actor.getSnapshot(), [actor]);
26+
const boundSelector: typeof selector = useCallback(
27+
(snapshot: Snapshot<any>) => {
28+
if (snapshot.status === 'error') {
29+
throw snapshot.error;
30+
}
31+
return selector(snapshot as never);
32+
},
33+
[selector]
34+
);
2335

2436
const selectedSnapshot = useSyncExternalStoreWithSelector(
2537
subscribe,
2638
boundGetSnapshot,
2739
boundGetSnapshot,
28-
selector,
40+
boundSelector,
2941
compare
3042
);
3143

0 commit comments

Comments
 (0)