UNPKG

react-rx

Version:
120 lines (110 loc) 4 kB
import {useCallback, useMemo, useSyncExternalStore} from 'react' import { asapScheduler, catchError, finalize, type Observable, type ObservedValueOf, of, share, timer, } from 'rxjs' import {map, tap} from 'rxjs/operators' function getValue<T>(value: T): T extends () => infer U ? U : T { return (typeof value === 'function' ? (value as () => any)() : value) as T extends () => infer U ? U : T } interface ObservableState<T> { didEmit: boolean snapshot?: T error?: unknown } interface CacheRecord<T> { observable: Observable<void> state: { didEmit: boolean snapshot?: T error?: unknown } getSnapshot: (initialValue: unknown) => T } const cache = new WeakMap<Observable<any>, CacheRecord<any>>() /** @public */ export function useObservable<ObservableType extends Observable<any>>( observable: ObservableType, initialValue: ObservedValueOf<ObservableType> | (() => ObservedValueOf<ObservableType>), ): ObservedValueOf<ObservableType> /** @public */ export function useObservable<ObservableType extends Observable<any>>( observable: ObservableType, ): undefined | ObservedValueOf<ObservableType> /** @public */ export function useObservable<ObservableType extends Observable<any>, InitialValue>( observable: ObservableType, initialValue: InitialValue | (() => InitialValue), ): InitialValue | ObservedValueOf<ObservableType> /** @public */ export function useObservable<ObservableType extends Observable<any>, InitialValue>( observable: ObservableType, initialValue?: InitialValue | (() => InitialValue), ): InitialValue | ObservedValueOf<ObservableType> { const instance = useMemo(() => { if (!cache.has(observable)) { // This separate object is used as a stable reference to the cache entry's snapshot and error. // It's used by the `getSnapshot` closure. const state: ObservableState<ObservedValueOf<ObservableType>> = { didEmit: false, } const entry: CacheRecord<ObservedValueOf<ObservableType>> = { state, observable: observable.pipe( map((value) => ({snapshot: value, error: undefined})), catchError((error) => of({snapshot: undefined, error})), tap(({snapshot, error}) => { state.didEmit = true state.snapshot = snapshot state.error = error }), // Note: any value or error emitted by the provided observable will be mapped to the cache entry's mutable state // and the observable is thereafter only used as a notifier to call `onStoreChange`, hence the `void` return type. map((value) => void value), // Ensure that the cache entry is deleted when the observable completes or errors. finalize(() => cache.delete(observable)), share({resetOnRefCountZero: () => timer(0, asapScheduler)}), ), getSnapshot: (initialValue) => { if (state.error) { throw state.error } return ( state.didEmit ? state.snapshot : getValue(initialValue) ) as ObservedValueOf<ObservableType> }, } // Eagerly subscribe to sync set `state.snapshot` to what the observable returns, and keep the observable alive until the component unmounts. const subscription = entry.observable.subscribe() subscription.unsubscribe() cache.set(observable, entry) } return cache.get(observable)! }, [observable]) const subscribe = useCallback( (onStoreChange: () => void) => { const subscription = instance.observable.subscribe(onStoreChange) return () => { subscription.unsubscribe() } }, [instance.observable], ) return useSyncExternalStore<ObservedValueOf<ObservableType>>( subscribe, () => { return instance.getSnapshot(initialValue) }, typeof initialValue === 'undefined' ? undefined : () => getValue(initialValue) as ObservedValueOf<ObservableType>, ) }