Skip to content

Commit c777db7

Browse files
josepotvoliva
andcommitted
fix: Subscribe with async errors
Co-authored-by: Victor Oliva <olivarra1@gmail.com>
1 parent b5826b4 commit c777db7

File tree

3 files changed

+65
-21
lines changed

3 files changed

+65
-21
lines changed

packages/core/src/Subscribe.test.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { state } from "@rxstate/core"
2-
import { render, screen } from "@testing-library/react"
2+
import { render, screen, act } from "@testing-library/react"
33
import React, { StrictMode, useState, useEffect } from "react"
44
import { defer, EMPTY, NEVER, Observable, of, startWith } from "rxjs"
55
import { bind, RemoveSubscribe, Subscribe as OriginalSubscribe } from "./"
66
import { TestErrorBoundary } from "./test-helpers/TestErrorBoundary"
77
import { useStateObservable } from "./useStateObservable"
88

9+
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms))
10+
911
const Subscribe = (props: any) => {
1012
return (
1113
<StrictMode>
@@ -303,6 +305,38 @@ describe("Subscribe", () => {
303305

304306
expect(hasError).toBe(false)
305307
})
308+
309+
it("allows async errors to be caught in error boundaries with suspense, without using source$", async () => {
310+
const [useError] = bind(
311+
new Observable((obs) => {
312+
setTimeout(() => obs.error("controlled error"), 10)
313+
}),
314+
)
315+
316+
const ErrorComponent = () => {
317+
const value = useError()
318+
return <>{value}</>
319+
}
320+
321+
const errorCallback = jest.fn()
322+
const { unmount } = render(
323+
<TestErrorBoundary onError={errorCallback}>
324+
<Subscribe fallback={<div>Loading...</div>}>
325+
<ErrorComponent />
326+
</Subscribe>
327+
</TestErrorBoundary>,
328+
)
329+
330+
await act(async () => {
331+
await wait(100)
332+
})
333+
334+
expect(errorCallback).toHaveBeenCalledWith(
335+
"controlled error",
336+
expect.any(Object),
337+
)
338+
unmount()
339+
})
306340
})
307341
})
308342

packages/core/src/Subscribe.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import React, {
88
useContext,
99
} from "react"
1010
import { Observable, Subscription } from "rxjs"
11+
import type { StateObservable } from "@rxstate/core"
1112

12-
const SubscriptionContext = createContext<Subscription | null>(null)
13+
const SubscriptionContext = createContext<
14+
((src: StateObservable<any>) => void) | null
15+
>(null)
1316
const { Provider } = SubscriptionContext
1417
export const useSubscription = () => useContext(SubscriptionContext)
1518

@@ -41,9 +44,27 @@ export const Subscribe: React.FC<{
4144
source$?: Observable<any>
4245
fallback?: NonNullable<ReactNode> | null
4346
}> = ({ source$, children, fallback }) => {
44-
const subscriptionRef = useRef<Subscription>()
47+
const subscriptionRef = useRef<{
48+
s: Subscription
49+
u: (source: StateObservable<any>) => void
50+
}>()
4551

46-
if (!subscriptionRef.current) subscriptionRef.current = new Subscription()
52+
if (!subscriptionRef.current) {
53+
const s = new Subscription()
54+
subscriptionRef.current = {
55+
s,
56+
u: (src) => {
57+
s.add(
58+
src.subscribe({
59+
error: (e) =>
60+
setSubscribedSource(() => {
61+
throw e
62+
}),
63+
}),
64+
)
65+
},
66+
}
67+
}
4768

4869
const [subscribedSource, setSubscribedSource] = useState<
4970
Observable<any> | null | undefined
@@ -77,14 +98,14 @@ export const Subscribe: React.FC<{
7798

7899
useEffect(() => {
79100
return () => {
80-
subscriptionRef.current?.unsubscribe()
101+
subscriptionRef.current?.s.unsubscribe()
81102
subscriptionRef.current = undefined
82103
}
83104
}, [])
84105

85106
const actualChildren =
86107
subscribedSource === source$ ? (
87-
<Provider value={subscriptionRef.current!}>{children}</Provider>
108+
<Provider value={subscriptionRef.current!.u}>{children}</Provider>
88109
) : fallback === undefined ? null : (
89110
<Throw />
90111
)

packages/core/src/useStateObservable.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useRef, useState } from "react"
22
import { SUSPENSE } from "./SUSPENSE"
33
import { DefaultedStateObservable, StateObservable } from "@rxstate/core"
4-
import { EMPTY_VALUE } from "./internal/empty-value"
54
import useSyncExternalStore from "./internal/useSyncExternalStore"
65
import { useSubscription } from "./Subscribe"
76

@@ -31,20 +30,10 @@ export const useStateObservable = <O>(
3130

3231
const gv: <T>() => Exclude<T, typeof SUSPENSE> = () => {
3332
const src = callbackRef.current!.source$ as DefaultedStateObservable<O>
34-
35-
if (src.getRefCount() > 0 || src.getDefaultValue) return getValue(src)
36-
37-
if (!subscription) throw new Error("Missing Subscribe!")
38-
39-
let error = EMPTY_VALUE
40-
subscription.add(
41-
src.subscribe({
42-
error: (e) => {
43-
error = e
44-
},
45-
}),
46-
)
47-
if (error !== EMPTY_VALUE) throw error
33+
if (!src.getRefCount() && !src.getDefaultValue) {
34+
if (!subscription) throw new Error("Missing Subscribe!")
35+
subscription(src)
36+
}
4837
return getValue(src)
4938
}
5039

0 commit comments

Comments
 (0)