diff --git a/packages/core/src/Subscribe.test.tsx b/packages/core/src/Subscribe.test.tsx index 06f32a3..df9f486 100644 --- a/packages/core/src/Subscribe.test.tsx +++ b/packages/core/src/Subscribe.test.tsx @@ -1,11 +1,13 @@ import { state } from "@rx-state/core" -import { render, screen } from "@testing-library/react" -import React, { StrictMode, useState, useEffect } from "react" +import { act, render, screen } from "@testing-library/react" +import React, { StrictMode, useEffect, useState } from "react" import { defer, EMPTY, NEVER, Observable, of, startWith } from "rxjs" import { bind, RemoveSubscribe, Subscribe as OriginalSubscribe } from "./" import { TestErrorBoundary } from "./test-helpers/TestErrorBoundary" import { useStateObservable } from "./useStateObservable" +const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)) + const Subscribe = (props: any) => { return ( @@ -303,6 +305,38 @@ describe("Subscribe", () => { expect(hasError).toBe(false) }) + + it("allows async errors to be caught in error boundaries with suspense, without using source$", async () => { + const [useError] = bind( + new Observable((obs) => { + setTimeout(() => obs.error("controlled error"), 10) + }), + ) + + const ErrorComponent = () => { + const value = useError() + return <>{value} + } + + const errorCallback = jest.fn() + const { unmount } = render( + + Loading...}> + + + , + ) + + await act(async () => { + await wait(100) + }) + + expect(errorCallback).toHaveBeenCalledWith( + "controlled error", + expect.any(Object), + ) + unmount() + }) }) }) diff --git a/packages/core/src/Subscribe.tsx b/packages/core/src/Subscribe.tsx index c14049c..9b49f41 100644 --- a/packages/core/src/Subscribe.tsx +++ b/packages/core/src/Subscribe.tsx @@ -8,8 +8,11 @@ import React, { useContext, } from "react" import { Observable, Subscription } from "rxjs" +import type { StateObservable } from "@rx-state/core" -const SubscriptionContext = createContext(null) +const SubscriptionContext = createContext< + ((src: StateObservable) => void) | null +>(null) const { Provider } = SubscriptionContext export const useSubscription = () => useContext(SubscriptionContext) @@ -41,9 +44,27 @@ export const Subscribe: React.FC<{ source$?: Observable fallback?: NonNullable | null }> = ({ source$, children, fallback }) => { - const subscriptionRef = useRef() + const subscriptionRef = useRef<{ + s: Subscription + u: (source: StateObservable) => void + }>() - if (!subscriptionRef.current) subscriptionRef.current = new Subscription() + if (!subscriptionRef.current) { + const s = new Subscription() + subscriptionRef.current = { + s, + u: (src) => { + s.add( + src.subscribe({ + error: (e) => + setSubscribedSource(() => { + throw e + }), + }), + ) + }, + } + } const [subscribedSource, setSubscribedSource] = useState< Observable | null | undefined @@ -77,14 +98,14 @@ export const Subscribe: React.FC<{ useEffect(() => { return () => { - subscriptionRef.current?.unsubscribe() + subscriptionRef.current?.s.unsubscribe() subscriptionRef.current = undefined } }, []) const actualChildren = subscribedSource === source$ ? ( - {children} + {children} ) : fallback === undefined ? null : ( ) diff --git a/packages/core/src/useStateObservable.ts b/packages/core/src/useStateObservable.ts index bb83b77..e8a7ed8 100644 --- a/packages/core/src/useStateObservable.ts +++ b/packages/core/src/useStateObservable.ts @@ -1,11 +1,10 @@ -import { useRef, useState } from "react" import { - SUSPENSE, DefaultedStateObservable, - StateObservable, liftEffects, + StateObservable, + SUSPENSE, } from "@rx-state/core" -import { EMPTY_VALUE } from "./internal/empty-value" +import { useRef, useState } from "react" import useSyncExternalStore from "./internal/useSyncExternalStore" import { useSubscription } from "./Subscribe" @@ -31,21 +30,11 @@ export const useStateObservable = ( } const gv: () => Exclude = () => { - const src = callbackRef.current!.source$ as DefaultedStateObservable - - if (src.getRefCount() > 0 || src.getDefaultValue) return getValue(src) - - if (!subscription) throw new Error("Missing Subscribe!") - - let error = EMPTY_VALUE - subscription.add( - liftEffects()(src).subscribe({ - error: (e) => { - error = e - }, - }), - ) - if (error !== EMPTY_VALUE) throw error + const src = callbackRef.current!.source$ as DefaultedStateObservable + if (!src.getRefCount() && !src.getDefaultValue) { + if (!subscription) throw new Error("Missing Subscribe!") + subscription(src) + } return getValue(src) }