feat(core): default value on bind

This commit is contained in:
Josep M Sobrepere 2020-10-15 09:24:06 +02:00
parent 09fde4b9ac
commit ca95e6a80f
7 changed files with 120 additions and 16 deletions

View File

@ -9,7 +9,7 @@ import {
Subject,
} from "rxjs"
import { renderHook, act as actHook } from "@testing-library/react-hooks"
import { switchMap, delay, take, catchError } from "rxjs/operators"
import { switchMap, delay, take, catchError, map } from "rxjs/operators"
import { FC, Suspense, useState } from "react"
import React from "react"
import {
@ -426,6 +426,53 @@ describe("connectFactoryObservable", () => {
unmount()
})
it("if the observable hasn't emitted and a defaultValue is provided, it does not start suspense", () => {
const number$ = new Subject<number>()
const [useNumber] = bind(
(id: number) => number$.pipe(map((x) => x + id)),
0,
)
const { result, unmount } = renderHook(() => useNumber(5))
expect(result.current).toBe(0)
actHook(() => {
number$.next(5)
})
expect(result.current).toBe(10)
unmount()
})
it("when a defaultValue is provided, the first subscription happens once the component is mounted", () => {
let nTopSubscriptions = 0
const [useNTopSubscriptions] = bind(
(id: number) =>
defer(() => {
return of(++nTopSubscriptions + id)
}),
1,
)
const { result, rerender, unmount } = renderHook(() =>
useNTopSubscriptions(0),
)
expect(result.current).toBe(1)
actHook(() => {
rerender()
})
expect(result.current).toBe(1)
expect(nTopSubscriptions).toBe(1)
unmount()
})
})
describe("observable", () => {

View File

@ -3,6 +3,7 @@ import shareLatest from "../internal/share-latest"
import reactEnhancer from "../internal/react-enhancer"
import { BehaviorObservable } from "../internal/BehaviorObservable"
import { useObservable } from "../internal/useObservable"
import { EMPTY_VALUE } from "../internal/empty-value"
import { SUSPENSE } from "../SUSPENSE"
/**
@ -26,6 +27,7 @@ import { SUSPENSE } from "../SUSPENSE"
*/
export default function connectFactoryObservable<A extends [], O>(
getObservable: (...args: A) => Observable<O>,
defaultValue: O = EMPTY_VALUE,
): [
(...args: A) => Exclude<O, typeof SUSPENSE>,
(...args: A) => Observable<O>,
@ -67,7 +69,7 @@ export default function connectFactoryObservable<A extends [], O>(
return source$.subscribe(subscriber)
}) as BehaviorObservable<O>
publicShared$.getValue = sharedObservable$.getValue
const reactGetValue = reactEnhancer(publicShared$)
const reactGetValue = reactEnhancer(publicShared$, defaultValue)
const result: [BehaviorObservable<O>, () => O] = [
publicShared$,

View File

@ -508,4 +508,45 @@ describe("connectObservable", () => {
expect(screen.queryByText("Loading...")).toBeNull()
expect(screen.queryByText("Hello")).not.toBeNull()
})
it("if the observable hasn't emitted and a defaultValue is provided, it does not start suspense", () => {
const number$ = new Subject<number>()
const [useNumber] = bind(number$, 0)
const { result, unmount } = renderHook(() => useNumber())
expect(result.current).toBe(0)
act(() => {
number$.next(5)
})
expect(result.current).toBe(5)
unmount()
})
it("when a defaultValue is provided, the first subscription happens once the component is mounted", () => {
let nTopSubscriptions = 0
const [useNTopSubscriptions] = bind(
defer(() => of(++nTopSubscriptions)),
1,
)
const { result, rerender, unmount } = renderHook(() =>
useNTopSubscriptions(),
)
expect(result.current).toBe(1)
act(() => {
rerender()
})
expect(result.current).toBe(1)
expect(nTopSubscriptions).toBe(1)
unmount()
})
})

View File

@ -2,6 +2,7 @@ import { Observable } from "rxjs"
import shareLatest from "../internal/share-latest"
import reactEnhancer from "../internal/react-enhancer"
import { useObservable } from "../internal/useObservable"
import { EMPTY_VALUE } from "../internal/empty-value"
/**
* Accepts: An Observable.
@ -19,9 +20,12 @@ import { useObservable } from "../internal/useObservable"
* for the first value.
*/
const emptyArr: Array<any> = []
export default function connectObservable<T>(observable: Observable<T>) {
export default function connectObservable<T>(
observable: Observable<T>,
defaultValue: T = EMPTY_VALUE,
) {
const sharedObservable$ = shareLatest<T>(observable, false)
const getValue = reactEnhancer(sharedObservable$)
const getValue = reactEnhancer(sharedObservable$, defaultValue)
const useStaticObservable = () =>
useObservable(sharedObservable$, getValue, emptyArr)
return [useStaticObservable, sharedObservable$] as const

View File

@ -19,6 +19,7 @@ import connectObservable from "./connectObservable"
*/
export function bind<T>(
observable: Observable<T>,
defaultValue?: T,
): [() => Exclude<T, typeof SUSPENSE>, Observable<T>]
/**
@ -41,12 +42,11 @@ export function bind<T>(
*/
export function bind<A extends unknown[], O>(
getObservable: (...args: A) => Observable<O>,
defaultValue?: O,
): [(...args: A) => Exclude<O, typeof SUSPENSE>, (...args: A) => Observable<O>]
export function bind<A extends unknown[], O>(
obs: ((...args: A) => Observable<O>) | Observable<O>,
) {
return (typeof obs === "function"
export function bind(...args: any[]) {
return (typeof args[0] === "function"
? (connectFactoryObservable as any)
: connectObservable)(obs)
: connectObservable)(...args)
}

View File

@ -2,15 +2,19 @@ import { SUSPENSE } from "../SUSPENSE"
import { BehaviorObservable } from "./BehaviorObservable"
import { EMPTY_VALUE } from "./empty-value"
const reactEnhancer = <T>(source$: BehaviorObservable<T>): (() => T) => {
const reactEnhancer = <T>(
source$: BehaviorObservable<T>,
defaultValue: T,
): (() => T) => {
let promise: Promise<T | void> | null
let error: any = EMPTY_VALUE
return (): T => {
const res = (): T => {
const currentValue = source$.getValue()
if (currentValue !== SUSPENSE && currentValue !== EMPTY_VALUE) {
return currentValue
}
if (defaultValue !== EMPTY_VALUE) return defaultValue
let timeoutToken
if (error !== EMPTY_VALUE) {
@ -56,6 +60,8 @@ const reactEnhancer = <T>(source$: BehaviorObservable<T>): (() => T) => {
throw error !== EMPTY_VALUE ? error : promise
}
res.d = defaultValue
return res
}
export default reactEnhancer

View File

@ -14,6 +14,7 @@ export const useObservable = <O>(
useEffect(() => {
let err: any = EMPTY_VALUE
let syncVal: O | typeof SUSPENSE = EMPTY_VALUE
const onError = (error: any) => {
err = error
setState(() => {
@ -26,13 +27,16 @@ export const useObservable = <O>(
}, onError)
if (err !== EMPTY_VALUE) return
const set = (val: O | (() => O)) => {
if (!Object.is(val, prevStateRef.current)) {
setState((prevStateRef.current = val))
}
const set = (value: O | (() => O)) => {
if (!Object.is(prevStateRef.current, value))
setState((prevStateRef.current = value))
}
const defaultValue = (getValue as any).d
if (syncVal === EMPTY_VALUE) {
set(defaultValue === EMPTY_VALUE ? getValue : defaultValue)
}
if (syncVal === EMPTY_VALUE) set(getValue)
const t = subscription
subscription = source$.subscribe((value: O | typeof SUSPENSE) => {
set(value === SUSPENSE ? getValue : value)