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 committed Jan 10, 2024
1 parent 2fed804 commit af0316f
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 9 deletions.
17 changes: 15 additions & 2 deletions packages/xstate-react/src/useActor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import isDevelopment from '#is-development';
import { useCallback, useEffect } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { Actor, ActorOptions, AnyActorLogic, SnapshotFrom } from 'xstate';
import {
Actor,
ActorOptions,
AnyActorLogic,
Snapshot,
SnapshotFrom
} from 'xstate';
import { stopRootWithRehydration } from './stopRootWithRehydration.ts';
import { useIdleActorRef } from './useActorRef.ts';

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

const subscribe = useCallback(
(handleStoreChange) => {
const { unsubscribe } = actorRef.subscribe(handleStoreChange);
const { unsubscribe } = actorRef.subscribe(
handleStoreChange,
handleStoreChange
);
return unsubscribe;
},
[actorRef]
Expand All @@ -40,6 +49,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 @@ -42,6 +43,8 @@ export function useIdleActorRef<TLogic extends AnyActorLogic>(
return actorRef;
}

const UNIQUE = {};

export function useActorRef<TLogic extends AnyActorLogic>(
machine: TLogic,
options: ActorOptions<TLogic> = {},
Expand All @@ -50,12 +53,23 @@ export function useActorRef<TLogic extends AnyActorLogic>(
| ((value: SnapshotFrom<TLogic>) => void)
): 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 { ActorRef, SnapshotFrom } from 'xstate';
import { ActorRef, Snapshot, SnapshotFrom } from 'xstate';

function defaultCompare<T>(a: T, b: T) {
return a === b;
Expand All @@ -13,19 +13,31 @@ export function useSelector<TActor extends ActorRef<any, any>, T>(
): T {
const subscribe = useCallback(
(handleStoreChange) => {
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 af0316f

Please sign in to comment.