import { useDebugValue, useEffect, useRef, useState } from "react"; import { Observable } from "rxjs"; import { useForceUpdate, useIsomorphicLayoutEffect } from "./helpers"; /** * Optimized for safely getting synchronous values from hot or pure observables * without triggering extra initial re-rendering. * * ⚠ If the observable is cold and with side effects * they will be performed at least twice! * * By default this hook will subscribe to the observable at least twice. * The first time is for getting synchronous value to prevent extra initial re-rendering. * In concurrent mode this may happen more than one time. * * @template TState State. * * @param state$ An observable of state value. */ export function useObservableEagerState( state$: Observable ): TState { const forceUpdate = useForceUpdate(); const state$Ref = useRef(state$); const errorRef = useRef(); const isAsyncEmissionRef = useRef(false); const didSyncEmit = useRef(false); const [state, setState] = useState(() => { let state: TState; state$ .subscribe({ next: value => { didSyncEmit.current = true; state = value; }, error: error => { errorRef.current = error; }, }) .unsubscribe(); return state!; }); // update the latest observable // synchronously after render being committed useIsomorphicLayoutEffect(() => { state$Ref.current = state$; }); useEffect(() => { errorRef.current = null; // keep in closure for checking staleness const input$ = state$Ref.current; let secondInitialValue = state; const subscription = input$.subscribe({ next: value => { if (input$ !== state$Ref.current) { // stale observable return; } if (isAsyncEmissionRef.current) { // ignore synchronous value // prevent initial re-rendering setState(() => value); } else { secondInitialValue = value; } }, error: error => { if (input$ !== state$Ref.current) { // stale observable return; } errorRef.current = error; forceUpdate(); }, }); if (!isAsyncEmissionRef.current) { // fix #86 where sync emission may happen before useEffect if (secondInitialValue !== state) { setState(() => secondInitialValue); } } isAsyncEmissionRef.current = true; return () => { subscription.unsubscribe(); }; }, [state$]); if (errorRef.current) { // Let error boundary catch the error throw errorRef.current; } if (didSyncEmit.current) { useDebugValue(state); return state; } else { throw new Error("Observable did not synchronously emit a value."); } }