Skip to content

Commit

Permalink
Throw errors from actor snapshots to trigger error boundaries in React
Browse files Browse the repository at this point in the history
  • Loading branch information
Andarist authored and davidkpiano committed Sep 12, 2024
1 parent c1ac639 commit 950e048
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 8 deletions.
10 changes: 9 additions & 1 deletion packages/xstate-react/src/useActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Actor,
ActorOptions,
AnyActorLogic,
Snapshot,
SnapshotFrom,
type ConditionalRequired,
type IsNotNever,
Expand Down Expand Up @@ -43,7 +44,10 @@ export function useActor<TLogic extends AnyActorLogic>(

const subscribe = useCallback(
(handleStoreChange: () => void) => {
const { unsubscribe } = actorRef.subscribe(handleStoreChange);
const { unsubscribe } = actorRef.subscribe(
handleStoreChange,
handleStoreChange
);
return unsubscribe;
},
[actorRef]
Expand All @@ -55,6 +59,10 @@ export function useActor<TLogic extends AnyActorLogic>(
getSnapshot
);

if ((actorSnapshot as Snapshot<any>).status === 'error') {
throw (actorSnapshot as Snapshot<any>).error;
}

useEffect(() => {
actorRef.start();

Expand Down
22 changes: 18 additions & 4 deletions packages/xstate-react/src/useActorRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
AnyActorLogic,
AnyStateMachine,
Observer,
Snapshot,
SnapshotFrom,
createActor,
toObserver,
Expand Down Expand Up @@ -52,6 +53,8 @@ export function useIdleActorRef<TLogic extends AnyActorLogic>(
return actorRef;
}

const UNIQUE = {};

export function useActorRef<TLogic extends AnyActorLogic>(
machine: TLogic,
...[options, observerOrListener]: IsNotNever<
Expand All @@ -73,12 +76,23 @@ export function useActorRef<TLogic extends AnyActorLogic>(
]
): Actor<TLogic> {
const actorRef = useIdleActorRef(machine, options);
const [reactError, setReactError] = useState(() => {
const initialSnapshot: Snapshot<any> = actorRef.getSnapshot();
return initialSnapshot.status === 'error' ? initialSnapshot.error : UNIQUE;
});

if (reactError !== UNIQUE) {
throw reactError;
}

useEffect(() => {
if (!observerOrListener) {
return;
}
let sub = actorRef.subscribe(toObserver(observerOrListener));
const observer = toObserver(observerOrListener);
const errorListener = observer.error;
observer.error = (error) => {
setReactError(error);
errorListener?.(error);
};
let sub = actorRef.subscribe(observer);
return () => {
sub.unsubscribe();
};
Expand Down
18 changes: 15 additions & 3 deletions packages/xstate-react/src/useSelector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
import { AnyActorRef, SnapshotFrom } from 'xstate';
import { AnyActorRef, Snapshot, SnapshotFrom } from 'xstate';

type SyncExternalStoreSubscribe = Parameters<
typeof useSyncExternalStoreWithSelector
Expand All @@ -27,19 +27,31 @@ export function useSelector<
if (!actor) {
return () => {};
}
const { unsubscribe } = actor.subscribe(handleStoreChange);
const { unsubscribe } = actor.subscribe(
handleStoreChange,
handleStoreChange
);
return unsubscribe;
},
[actor]
);

const boundGetSnapshot = useCallback(() => actor?.getSnapshot(), [actor]);
const boundSelector: typeof selector = useCallback(
(snapshot: Snapshot<any>) => {
if (snapshot.status === 'error') {
throw snapshot.error;
}
return selector(snapshot as never);
},
[selector]
);

const selectedSnapshot = useSyncExternalStoreWithSelector(
subscribe,
boundGetSnapshot,
boundGetSnapshot,
selector,
boundSelector,
compare
);

Expand Down

0 comments on commit 950e048

Please sign in to comment.