Skip to content

Commit fc554cd

Browse files
committed
feat: rewrite useSubscribe
1 parent 2dfa01f commit fc554cd

7 files changed

Lines changed: 110 additions & 83 deletions

File tree

src/lib/binding/useSubscribe.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/lib/binding/useSubscribe.test.tsx renamed to src/lib/binding/useSubscribe/useSubscribe.test.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { act, cleanup, renderHook } from "@testing-library/react"
2-
import { useEffect, useState } from "react"
2+
import { useCallback, useEffect, useState } from "react"
33
import { type Observable, of, tap } from "rxjs"
44
import { afterEach, describe, expect, it } from "vitest"
5-
import { waitForTimeout } from "../../tests/utils"
5+
import { waitForTimeout } from "../../../tests/utils"
66
import { useSubscribe } from "./useSubscribe"
77

88
afterEach(() => {
@@ -14,7 +14,7 @@ describe("Given a function that returns undefined", () => {
1414
const maybeObservableFn = () => undefined as Observable<number> | undefined
1515

1616
renderHook(() => {
17-
useSubscribe(maybeObservableFn, [])
17+
useSubscribe(maybeObservableFn)
1818
}, {})
1919

2020
expect(true).toBe(true)
@@ -29,7 +29,7 @@ describe("Given a function that returns undefined", () => {
2929
undefined,
3030
)
3131

32-
useSubscribe(
32+
const source$ = useCallback(
3333
() =>
3434
value?.pipe(
3535
tap((v) => {
@@ -39,6 +39,8 @@ describe("Given a function that returns undefined", () => {
3939
[value],
4040
)
4141

42+
useSubscribe(source$)
43+
4244
useEffect(() => {
4345
setTimeout(() => {
4446
setValue(of(5))
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useEffect } from "react"
2+
import { makeObservable } from "../../utils/makeObservable"
3+
import type { SubscribeSource } from "./types"
4+
5+
export function useSubscribe<T>(source: SubscribeSource<T>) {
6+
useEffect(() => {
7+
const sub = makeObservable(source)().subscribe()
8+
9+
return () => {
10+
sub.unsubscribe()
11+
}
12+
}, [source])
13+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { act, cleanup, renderHook } from "@testing-library/react"
2+
import { config, defer, of, throwError } from "rxjs"
3+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
4+
import { waitForTimeout } from "../../tests/utils"
5+
import { useSubscribeEffect } from "./useSubscribeEffect"
6+
7+
let previousOnUnhandledError = config.onUnhandledError
8+
9+
beforeEach(() => {
10+
previousOnUnhandledError = config.onUnhandledError
11+
config.onUnhandledError = () => {}
12+
})
13+
14+
afterEach(() => {
15+
config.onUnhandledError = previousOnUnhandledError
16+
cleanup()
17+
vi.restoreAllMocks()
18+
})
19+
20+
describe("useSubscribeEffect", () => {
21+
it("should call onError when source errors", async () => {
22+
const onError = vi.fn()
23+
let subscribeCount = 0
24+
25+
const source$ = defer(() => {
26+
subscribeCount += 1
27+
return subscribeCount === 1 ? throwError(() => new Error("boom")) : of(1)
28+
})
29+
30+
renderHook(() => {
31+
useSubscribeEffect(source$, { onError, retryOnError: true })
32+
})
33+
34+
await act(async () => {
35+
await waitForTimeout(0)
36+
})
37+
38+
expect(onError).toHaveBeenCalledTimes(1)
39+
expect(subscribeCount).toBe(2)
40+
})
41+
42+
it("should not retry when retryOnError is false", async () => {
43+
const onError = vi.fn()
44+
const onUnhandledError = vi.fn()
45+
let subscribeCount = 0
46+
config.onUnhandledError = onUnhandledError
47+
48+
const source$ = defer(() => {
49+
subscribeCount += 1
50+
return throwError(() => new Error("boom"))
51+
})
52+
53+
renderHook(() => {
54+
useSubscribeEffect(source$, { onError, retryOnError: false })
55+
})
56+
57+
await act(async () => {
58+
await waitForTimeout(0)
59+
})
60+
61+
expect(onError).toHaveBeenCalledTimes(1)
62+
expect(onUnhandledError).toHaveBeenCalledTimes(1)
63+
expect(onUnhandledError.mock.calls[0]?.[0]).toBeInstanceOf(Error)
64+
expect(subscribeCount).toBe(1)
65+
})
66+
})
Lines changed: 22 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,36 @@
1-
import { type DependencyList, useCallback } from "react"
2-
import { catchError, identity, retry } from "rxjs"
1+
import { useCallback } from "react"
2+
import { of, retry, tap, throwError } from "rxjs"
33
import { makeObservable } from "../utils/makeObservable"
44
import { useLiveRef } from "../utils/react/useLiveRef"
5-
import type { SubscribeSource } from "./types"
6-
import { useSubscribe } from "./useSubscribe"
5+
import type { SubscribeSource } from "./useSubscribe/types"
6+
import { useSubscribe } from "./useSubscribe/useSubscribe"
77

88
interface Option {
9-
retry?: boolean
9+
retryOnError?: boolean
1010
onError?: (error: unknown) => void
1111
}
1212

13-
export function useSubscribeEffect<T>(source: SubscribeSource<T>): void
1413
export function useSubscribeEffect<T>(
1514
source: SubscribeSource<T>,
16-
options: Option,
17-
): void
18-
19-
export function useSubscribeEffect<T>(
20-
source: SubscribeSource<T>,
21-
deps: DependencyList,
22-
): void
23-
24-
export function useSubscribeEffect<T>(
25-
source: SubscribeSource<T>,
26-
options: Option,
27-
deps: DependencyList,
28-
): void
29-
30-
export function useSubscribeEffect<T>(
31-
source: SubscribeSource<T>,
32-
unsafeOptions?: Option | DependencyList,
33-
deps: DependencyList = [],
15+
options?: Option,
3416
) {
35-
const options =
36-
unsafeOptions != null && !Array.isArray(unsafeOptions)
37-
? (unsafeOptions as Option)
38-
: ({} satisfies Option)
39-
const retryOption = options.retry ?? true
40-
const onErrorRef = useLiveRef(
41-
options.onError ??
42-
((error: unknown) => {
43-
console.error(error)
44-
}),
45-
)
17+
const optionsRef = useLiveRef(options)
4618

47-
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO
48-
const sourceAsObservable = useCallback(() => makeObservable(source)(), deps)
19+
const enhancerMakeObservable = useCallback(() => {
20+
const source$ = makeObservable(source)()
4921

50-
const enhancerMakeObservable = useCallback(
51-
() =>
52-
sourceAsObservable().pipe(
53-
catchError((error) => {
54-
onErrorRef.current(error)
55-
56-
throw error
57-
}),
58-
retryOption ? retry() : identity,
59-
),
60-
[sourceAsObservable, retryOption, onErrorRef],
61-
)
22+
return source$.pipe(
23+
tap({
24+
error: (error) => {
25+
optionsRef.current?.onError?.(error)
26+
},
27+
}),
28+
retry({
29+
delay: (error) =>
30+
optionsRef.current?.retryOnError ? of(0) : throwError(() => error),
31+
}),
32+
)
33+
}, [source, optionsRef])
6234

63-
useSubscribe(enhancerMakeObservable, deps)
35+
useSubscribe(enhancerMakeObservable)
6436
}

vite.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ export default defineConfig({
2121
devDeps: true,
2222
}),
2323
},
24-
dts(),
24+
dts({
25+
staticImport: true,
26+
}),
2527
],
2628
build: {
2729
minify: "terser",

0 commit comments

Comments
 (0)