Skip to content

Commit ca95e6a

Browse files
committed
feat(core): default value on bind
1 parent 09fde4b commit ca95e6a

7 files changed

+120
-16
lines changed

packages/core/src/bind/connectFactoryObservable.test.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
Subject,
1010
} from "rxjs"
1111
import { renderHook, act as actHook } from "@testing-library/react-hooks"
12-
import { switchMap, delay, take, catchError } from "rxjs/operators"
12+
import { switchMap, delay, take, catchError, map } from "rxjs/operators"
1313
import { FC, Suspense, useState } from "react"
1414
import React from "react"
1515
import {
@@ -426,6 +426,53 @@ describe("connectFactoryObservable", () => {
426426

427427
unmount()
428428
})
429+
430+
it("if the observable hasn't emitted and a defaultValue is provided, it does not start suspense", () => {
431+
const number$ = new Subject<number>()
432+
const [useNumber] = bind(
433+
(id: number) => number$.pipe(map((x) => x + id)),
434+
0,
435+
)
436+
437+
const { result, unmount } = renderHook(() => useNumber(5))
438+
439+
expect(result.current).toBe(0)
440+
441+
actHook(() => {
442+
number$.next(5)
443+
})
444+
445+
expect(result.current).toBe(10)
446+
447+
unmount()
448+
})
449+
450+
it("when a defaultValue is provided, the first subscription happens once the component is mounted", () => {
451+
let nTopSubscriptions = 0
452+
453+
const [useNTopSubscriptions] = bind(
454+
(id: number) =>
455+
defer(() => {
456+
return of(++nTopSubscriptions + id)
457+
}),
458+
1,
459+
)
460+
461+
const { result, rerender, unmount } = renderHook(() =>
462+
useNTopSubscriptions(0),
463+
)
464+
465+
expect(result.current).toBe(1)
466+
467+
actHook(() => {
468+
rerender()
469+
})
470+
471+
expect(result.current).toBe(1)
472+
expect(nTopSubscriptions).toBe(1)
473+
474+
unmount()
475+
})
429476
})
430477

431478
describe("observable", () => {

packages/core/src/bind/connectFactoryObservable.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import shareLatest from "../internal/share-latest"
33
import reactEnhancer from "../internal/react-enhancer"
44
import { BehaviorObservable } from "../internal/BehaviorObservable"
55
import { useObservable } from "../internal/useObservable"
6+
import { EMPTY_VALUE } from "../internal/empty-value"
67
import { SUSPENSE } from "../SUSPENSE"
78

89
/**
@@ -26,6 +27,7 @@ import { SUSPENSE } from "../SUSPENSE"
2627
*/
2728
export default function connectFactoryObservable<A extends [], O>(
2829
getObservable: (...args: A) => Observable<O>,
30+
defaultValue: O = EMPTY_VALUE,
2931
): [
3032
(...args: A) => Exclude<O, typeof SUSPENSE>,
3133
(...args: A) => Observable<O>,
@@ -67,7 +69,7 @@ export default function connectFactoryObservable<A extends [], O>(
6769
return source$.subscribe(subscriber)
6870
}) as BehaviorObservable<O>
6971
publicShared$.getValue = sharedObservable$.getValue
70-
const reactGetValue = reactEnhancer(publicShared$)
72+
const reactGetValue = reactEnhancer(publicShared$, defaultValue)
7173

7274
const result: [BehaviorObservable<O>, () => O] = [
7375
publicShared$,

packages/core/src/bind/connectObservable.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,4 +508,45 @@ describe("connectObservable", () => {
508508
expect(screen.queryByText("Loading...")).toBeNull()
509509
expect(screen.queryByText("Hello")).not.toBeNull()
510510
})
511+
512+
it("if the observable hasn't emitted and a defaultValue is provided, it does not start suspense", () => {
513+
const number$ = new Subject<number>()
514+
const [useNumber] = bind(number$, 0)
515+
516+
const { result, unmount } = renderHook(() => useNumber())
517+
518+
expect(result.current).toBe(0)
519+
520+
act(() => {
521+
number$.next(5)
522+
})
523+
524+
expect(result.current).toBe(5)
525+
526+
unmount()
527+
})
528+
529+
it("when a defaultValue is provided, the first subscription happens once the component is mounted", () => {
530+
let nTopSubscriptions = 0
531+
532+
const [useNTopSubscriptions] = bind(
533+
defer(() => of(++nTopSubscriptions)),
534+
1,
535+
)
536+
537+
const { result, rerender, unmount } = renderHook(() =>
538+
useNTopSubscriptions(),
539+
)
540+
541+
expect(result.current).toBe(1)
542+
543+
act(() => {
544+
rerender()
545+
})
546+
547+
expect(result.current).toBe(1)
548+
expect(nTopSubscriptions).toBe(1)
549+
550+
unmount()
551+
})
511552
})

packages/core/src/bind/connectObservable.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Observable } from "rxjs"
22
import shareLatest from "../internal/share-latest"
33
import reactEnhancer from "../internal/react-enhancer"
44
import { useObservable } from "../internal/useObservable"
5+
import { EMPTY_VALUE } from "../internal/empty-value"
56

67
/**
78
* Accepts: An Observable.
@@ -19,9 +20,12 @@ import { useObservable } from "../internal/useObservable"
1920
* for the first value.
2021
*/
2122
const emptyArr: Array<any> = []
22-
export default function connectObservable<T>(observable: Observable<T>) {
23+
export default function connectObservable<T>(
24+
observable: Observable<T>,
25+
defaultValue: T = EMPTY_VALUE,
26+
) {
2327
const sharedObservable$ = shareLatest<T>(observable, false)
24-
const getValue = reactEnhancer(sharedObservable$)
28+
const getValue = reactEnhancer(sharedObservable$, defaultValue)
2529
const useStaticObservable = () =>
2630
useObservable(sharedObservable$, getValue, emptyArr)
2731
return [useStaticObservable, sharedObservable$] as const

packages/core/src/bind/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import connectObservable from "./connectObservable"
1919
*/
2020
export function bind<T>(
2121
observable: Observable<T>,
22+
defaultValue?: T,
2223
): [() => Exclude<T, typeof SUSPENSE>, Observable<T>]
2324

2425
/**
@@ -41,12 +42,11 @@ export function bind<T>(
4142
*/
4243
export function bind<A extends unknown[], O>(
4344
getObservable: (...args: A) => Observable<O>,
45+
defaultValue?: O,
4446
): [(...args: A) => Exclude<O, typeof SUSPENSE>, (...args: A) => Observable<O>]
4547

46-
export function bind<A extends unknown[], O>(
47-
obs: ((...args: A) => Observable<O>) | Observable<O>,
48-
) {
49-
return (typeof obs === "function"
48+
export function bind(...args: any[]) {
49+
return (typeof args[0] === "function"
5050
? (connectFactoryObservable as any)
51-
: connectObservable)(obs)
51+
: connectObservable)(...args)
5252
}

packages/core/src/internal/react-enhancer.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ import { SUSPENSE } from "../SUSPENSE"
22
import { BehaviorObservable } from "./BehaviorObservable"
33
import { EMPTY_VALUE } from "./empty-value"
44

5-
const reactEnhancer = <T>(source$: BehaviorObservable<T>): (() => T) => {
5+
const reactEnhancer = <T>(
6+
source$: BehaviorObservable<T>,
7+
defaultValue: T,
8+
): (() => T) => {
69
let promise: Promise<T | void> | null
710
let error: any = EMPTY_VALUE
811

9-
return (): T => {
12+
const res = (): T => {
1013
const currentValue = source$.getValue()
1114
if (currentValue !== SUSPENSE && currentValue !== EMPTY_VALUE) {
1215
return currentValue
1316
}
17+
if (defaultValue !== EMPTY_VALUE) return defaultValue
1418

1519
let timeoutToken
1620
if (error !== EMPTY_VALUE) {
@@ -56,6 +60,8 @@ const reactEnhancer = <T>(source$: BehaviorObservable<T>): (() => T) => {
5660

5761
throw error !== EMPTY_VALUE ? error : promise
5862
}
63+
res.d = defaultValue
64+
return res
5965
}
6066

6167
export default reactEnhancer

packages/core/src/internal/useObservable.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const useObservable = <O>(
1414
useEffect(() => {
1515
let err: any = EMPTY_VALUE
1616
let syncVal: O | typeof SUSPENSE = EMPTY_VALUE
17+
1718
const onError = (error: any) => {
1819
err = error
1920
setState(() => {
@@ -26,13 +27,16 @@ export const useObservable = <O>(
2627
}, onError)
2728
if (err !== EMPTY_VALUE) return
2829

29-
const set = (val: O | (() => O)) => {
30-
if (!Object.is(val, prevStateRef.current)) {
31-
setState((prevStateRef.current = val))
32-
}
30+
const set = (value: O | (() => O)) => {
31+
if (!Object.is(prevStateRef.current, value))
32+
setState((prevStateRef.current = value))
33+
}
34+
35+
const defaultValue = (getValue as any).d
36+
if (syncVal === EMPTY_VALUE) {
37+
set(defaultValue === EMPTY_VALUE ? getValue : defaultValue)
3338
}
3439

35-
if (syncVal === EMPTY_VALUE) set(getValue)
3640
const t = subscription
3741
subscription = source$.subscribe((value: O | typeof SUSPENSE) => {
3842
set(value === SUSPENSE ? getValue : value)

0 commit comments

Comments
 (0)