Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions src/internal/react-enhancer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Observable, of, Subscription, Subject, race } from "rxjs"
import { delay, takeUntil, take, filter, tap } from "rxjs/operators"
import { delay, takeUntil, take, filter, tap, catchError } from "rxjs/operators"
import { SUSPENSE } from "../SUSPENSE"
import { BehaviorObservable } from "./BehaviorObservable"
import { EMPTY_VALUE } from "./empty-value"
Expand Down Expand Up @@ -57,15 +57,23 @@ const reactEnhancer = <T>(
}) as BehaviorObservable<T>

let promise: any
let hasError = false
let error: unknown = undefined
const getValue = () => {
if (hasError) {
throw error
}
Comment on lines +63 to +65
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I compared with the way that this is solved in React's example and within their read (our getValue() equivalent) they either return the value, throw the promise, or throw the error.

try {
return (source$ as BehaviorObservable<T>).getValue()
} catch (e) {
if (promise) throw promise

if (!IS_SSR && e !== SUSPENSE) {
source$
.pipe(takeUntil(race(onSubscribe, of(true).pipe(delay(60000)))))
.pipe(
takeUntil(race(onSubscribe, of(true).pipe(delay(60000)))),
catchError(() => of(true)),
)
.subscribe()
Comment on lines +73 to 77
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was needed because otherwise this subscription would receive an error without handling it - Raising an uncaught exception that jest would pick up as error even if (expect(true).toBe(true)).

try {
return (source$ as BehaviorObservable<T>).getValue()
Expand All @@ -76,9 +84,15 @@ const reactEnhancer = <T>(
.pipe(
filter(value => value !== (SUSPENSE as any)),
take(1),
tap(() => {
promise = undefined
}),
tap(
() => {
promise = undefined
},
err => {
hasError = true
error = err
},
),
)
.toPromise()
throw promise
Expand Down
33 changes: 33 additions & 0 deletions test/connectObservable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,39 @@ describe("connectObservable", () => {
)
})

it("allows errors to be caught in error boundaries with suspense", async () => {
const errStream = new Subject()
const [useError] = connectObservable(errStream)

const ErrorComponent = () => {
const value = useError()

return <>{value}</>
}

const errorCallback = jest.fn()
render(
<TestErrorBoundary onError={errorCallback}>
<Suspense fallback={<div>Loading...</div>}>
<ErrorComponent />
</Suspense>
</TestErrorBoundary>,
)

componentAct(() => {
errStream.error("controlled error")
})

await componentAct(async () => {
await wait(0)
})
Comment on lines +292 to +294
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ewww.

I couldn't find any doc on this, but by experimenting, it seems that when a thrown promise fails in Suspense:

  • It creates a new component instance. Yep, the reducer init function gets called, and if you have a setState(() => Math.random()) you'll get another value.
  • It retries rendering the component a couple of times.

It looks like the single componentAct can't pick this up. Chaining two toghether does fix it.

Do you smell that smelly smell?


expect(errorCallback).toHaveBeenCalledWith(
"controlled error",
expect.any(Object),
)
})

it("doesn't throw errors on components that will get unmounted on the next cycle", () => {
const valueStream = new BehaviorSubject(1)
const [useValue, value$] = connectObservable(valueStream)
Expand Down