Repository navigation
react-rx@7.0.0
Major Changes
-
#515
8ca73bdThanks @stipsan! - v7 moves every subscription out of the render phase and removesuseObservableEvent. Requirements are unchanged from v6: React^19.2, RxJS^7.2, Node>=22.12, ESM only. The v6 to v7 migration guide has more patterns.useObservableanduseSyncObservablerequire theinitialValueargument. The initial value is what renders until the observable emits. Omitting it is a type error, and because JavaScript callers can bypass the types, the hooks also throw aTypeErrorduring render when the argument is missing. Every value is valid,undefinedincluded, but you must pass it. Functions act as initializers, likeuseState. To use a function as the initial value, pass an initializer that returns it. The deprecated single-argument overloads are gone.// v6 const users = useObservable(users$) // v7 const users = useObservable(users$, undefined)
Call sites that already pass an
initialValuekeep their behavior. If an observable has no meaningful initial value, useuseObservablePromisewithuse()and Suspense instead.useObservableanduseSyncObservablenever subscribe during render. The render-phase warm-up is gone. Every render, the first one and every identity change alike, shows the resolvedinitialValue, or the shared cache entry's last emission when the observable is already live elsewhere. The subscription starts when the component commits, and a synchronous emission replaces theinitialValueright after that commit.- A synchronous emission no longer wins the first paint. v6 subscribed during render when no
initialValuewas given, soof,startWith, or aBehaviorSubjectcould render on the first pass. In v7 theinitialValuerenders first. - Swapping to a new observable renders the
initialValuefor one pass. The new observable's synchronous emission arrives after the swap commits.useObservablestill never renders the previous observable's value under the new identity. - Observable identities must be stable across renders. Use
useMemo,useState, module scope, or React Compiler memoization. An observable rebuilt on every render is torn down and re-subscribed on every render. When such a source synchronously replays a value that differs from theinitialValue, every commit forces a re-render and React aborts with "Maximum update depth exceeded". The v6 warm-up let that pattern converge. - A synchronously erroring observable surfaces its error after commit instead of during render.
disabled: truenow always guarantees zero subscriptions. In v6 the warm-up probe still subscribed once when noinitialValuewas given.- Server rendering renders the resolved
initialValueand never subscribes.useSyncObservablecan no longer hit React's "Missing getServerSnapshot" error, and a non-deterministic synchronous emission can no longer cause a hydration mismatch.
useObservablePromisenever subscribes during a mounting render. v6 started the source subscription during render unlessdisabled, so any render fetched as a side effect, hidden<Activity>pre-renders included. A fetch now has three triggers.- A non-
disabledcomponent that called the hook commits, on mount or on an<Activity>reveal. - A committed, visible, non-
disabledconsumer re-renders with a new observable identity. The hook subscribes the new source during that render, so a suspended transition can settle, retry, and commit. - Something calls
preloadObservablePromise.
This affects callers as follows.
use(useObservablePromise(obs$))inside one component deadlocks. The component suspends on its own pending promise before the commit that would start the fetch. This was never a supported pattern and is not guarded against. Pass the promise to a child that reads it withuse(), and put a<Suspense>boundary between the hook caller and that child.- A hidden
<Activity>tree that calls the hook is paused. Nothing subscribes until the tree is revealed and its effects mount. To pre-render hidden content with data, call the hook in a visible parent and pass the promise into the hidden tree. - A synchronously emitting source (
of,BehaviorSubject, a replayedshareReplay) resolves at the hook caller's commit, so a cold mount shows one Suspense fallback pass. Preload the observable to skip the fallback. - Swapping observables inside
startTransitionor behinduseDeferredValueneeds no preload. The live consumer's swap render starts the fetch, the previous content stays visible until the new data settles, and the swap then commits.preloadObservablePromisestill lets a swap commit with no pending period, for example after a hover preload. - A live consumer that flips
disabledoff inside a transition starts the fetch from that render. - A transition abandoned after its swap render can leave a fetch nobody consumes. The entry settles into the shared cache and is evicted after
ttl. Bound sources that may never settle with RxJStimeout, as you would forpreloadObservablePromise. - On the server,
preloadObservablePromiseis a no-op that returns an inert pending promise. It neither subscribes nor touches the cache. Server rendering emits the Suspense fallback and the fetch starts after hydration. For React Server Components or server-only flows, fetch withawaitor RxJSfirstValueFromand pass the promise or value as a prop.
useObservableEventis removed. Replace it withuseObservableSubject, available since v6.1, which returns the event stream and a stable handler that pushes into it. Then move the pipeline to where its output is consumed. A pipeline that ended intap(setState)becomes a derived stream read withuseObservableoruseSyncObservable. A side-effect-only pipeline subscribes in an effect. A pipeline that only forwarded into aSubjectbecomes a directnextcall.// v6 const [value, setValue] = useState(1) const handleChange = useObservableEvent((value$) => value$.pipe(map(Number), tap(setValue))) // v7 const [input$, handleChange] = useObservableSubject<string>() const value$ = useMemo(() => input$.pipe(map(Number)), [input$]) const value = useSyncObservable(value$, 1)
The
use-effect-eventdependency existed only for this hook and is gone. react-rx has no runtime dependencies beyond itsreactandrxjspeers. - A synchronous emission no longer wins the first paint. v6 subscribed during render when no