React hooks
useObservable()
A React hook that returns the current/latest value from an observable.
Store updates are deferred by default via useDeferredValue. Urgent renders keep the previous value while a background render catches up. That makes it safe to suspend on the returned value: already-revealed UI never gets replaced by a Suspense fallback.
The deferral is identity-coherent. The observable and its value defer as one snapshot. When the observable identity changes (say, it is memoized on a document id that just changed), the hook falls back to the live value: typically the new observable’s synchronous emission, or the initialValue. The previous identity’s value can never render under the new one. A bare useDeferredValue(useObservable(...)) cannot guarantee this.
More guarantees:
- Mounts, remounts, and
<Activity>reveals render the current snapshot synchronously. No initial-value flash. - On the server, the hook renders exactly what the client’s first paint will show: a synchronous emission when there is one, else the resolved
initialValue, else nothing. It never throws for a missinginitialValue.
Prefer this hook for previews, validation, lists, and other non-input reads.
Signature
function useObservable<T>(observable$: Observable<T>): T | undefined
function useObservable<T>(
observable$: Observable<T>,
initialValue: T | (() => T),
options?: UseObservableOptions,
): T
interface UseObservableOptions {
disabled?: boolean
}disabled pauses the live subscription. Later emissions stop updating the component, and the last value is kept. It does not skip the render-phase warm-up subscription. See the guide for swapping the observable when you need zero subscriptions.
When not to use
- Controlled inputs, or values read back in the same event. Deferred updates can lag the caret or drop keystrokes under load. Use
useSyncObservable. - One-shot async data where “loading” is a Suspense fallback. Emitting
{loading: true}placeholder values duplicates what<Suspense>already expresses. UseuseObservablePromise. - Plain values with no stream involved. Don’t wrap values in
of()or aSubjectjust to use the hook.useStateor props are simpler and faster. - Observables created fresh on every render. The hook caches by reference identity, so an unstable observable resubscribes every render. Memoize it, hoist it, or rely on the React Compiler. See keep observables referentially stable.
Example
import {useMemo} from 'react'
import {useObservable} from 'react-rx'
import {interval} from 'rxjs'
function MyComponent() {
const observable = useMemo(() => interval(100), [])
const number = useObservable(observable, 0)
return <>The number is {number}</>
}useSyncObservable()
A React hook that returns the current/latest value from an observable synchronously, via useSyncExternalStore. This is the v4 useObservable behavior.
Use it when:
- The value feeds a controlled input. The caret and IME need synchronous updates.
- The value must stay consistent within the same event, like an equality check before a write.
- You need strict control over server markup. The server renders the resolved
initialValue, and throws without one.
Signature
function useSyncObservable<T>(observable$: Observable<T>): T | undefined
function useSyncObservable<T>(
observable$: Observable<T>,
initialValue: T | (() => T),
options?: UseObservableOptions,
): TWhen not to use
- As the default for lists, previews, and other reads. Synchronous store mutations cannot be marked as Transitions. A suspending child replaces already-visible content with the nearest Suspense fallback. See the useSyncExternalStore caveats , and compare both hooks in the Suspense example. Use
useObservableinstead. - Fetch-loading UI. Use
useObservablePromiseand let<Suspense>own the fallback. - Wrapped in
useDeferredValue.useDeferredValue(useSyncObservable(...))is justuseObservable, minus its identity-coherence guarantee.
Example
import {useState} from 'react'
import {useSyncObservable} from 'react-rx'
import {Subject} from 'rxjs'
function SearchField() {
const [text$] = useState(() => new Subject<string>())
// Controlled input values must update synchronously.
const text = useSyncObservable(text$, '')
return <input value={text} onChange={(e) => text$.next(e.currentTarget.value)} />
}useObservablePromise()
A React hook that turns an observable into a use()-compatible promise, for Suspense and Activity pre-rendering.
Signature
function useObservablePromise<T>(
observable: Observable<T>,
options?: UseObservablePromiseOptions,
): ObservablePromise<T>
interface UseObservablePromiseOptions {
disabled?: boolean
ttl?: number
}
type ObservablePromise<T> = Promise<T> &
({status: 'pending'} | {status: 'fulfilled'; value: T} | {status: 'rejected'; reason: unknown})How it behaves:
- The hook does not suspend. Pass the returned promise to React’s
useinside a<Suspense>boundary. - The reader suspends until the first emission. Later emissions update without re-suspending.
- Errors reject the promise, which surfaces at the Error Boundary.
See the guide for the startWith caveat, disabled and ttl, and when to prefer useObservable.
When not to use
- Streams that
startWith(...)a placeholder. The placeholder counts as the first emission. The promise fulfills instantly with it, and Suspense never shows. Drop thestartWith, or useuseObservablewith the placeholder asinitialValue. - Live values that should render immediately, without a boundary. Use
useObservable. - Controlled inputs. Use
useSyncObservable. - Unstable observable identity. Every new observable reference is a new pending promise, which re-triggers the fallback. Keep the observable stable. Prefer creating the promise in a parent that does not itself suspend.
Example
import {Suspense, use, useMemo} from 'react'
import {useObservablePromise} from 'react-rx'
import {fromFetch} from 'rxjs/fetch'
function Profile({url}: {url: string}) {
const data$ = useMemo(() => fromFetch(url, {selector: (r) => r.json()}), [url])
const promise = useObservablePromise(data$)
return (
<Suspense fallback="Loading…">
<Pre promise={promise} />
</Suspense>
)
}
function Pre({promise}: {promise: Promise<unknown>}) {
return <pre>{JSON.stringify(use(promise), null, 2)}</pre>
}preloadObservablePromise()
Warms the useObservablePromise cache outside of rendering, for example on mouseenter or in a route loader. Not a hook. Callable anywhere. Returns the same promise instance the hook would return for that observable.
Calling it starts the source subscription immediately. Pending entries are never timed out. A never-emitting or hung observable keeps both the promise and the subscription alive until it settles. When a preload can stall, bound it with RxJS timeout, or cancel the source.
Signature
function preloadObservablePromise<T>(
observable: Observable<T>,
options?: {ttl?: number},
): ObservablePromise<T>Default ttl is 5000, longer than the hook default, so a hover-warmed value survives until click or navigation.
When not to use
- Sources that may never settle, unless you bound them first. The subscription stays alive until the promise settles. Add a
timeoutor make the source cancellable. - As a data-reading mechanism. It only warms the cache. Components still read through
useObservablePromiseanduse().
useObservableEvent()
A React hook that turns an event handler into an observable stream. You pass a function that receives an observable of events and returns an observable of side effects. The hook returns a stable callback for DOM or component event props. Each call of the callback emits into the pipeline, which stays subscribed for the lifetime of the component.
Prefer an explicit Subject instead. useObservableEvent hides both the subscription and the data flow inside the hook. Values vanish into tap side effects. The pipeline cannot be composed with anything else. The same wiring is clearer when events push into a Subject you can see, and behavior lives on streams derived from it. See working with events in the guide. Reserve this hook for pipelines that are genuinely event-first, per-component, and side-effect-only.
Signature
function useObservableEvent<T, U>(
handleEvent: (arg: Observable<T>) => Observable<U>,
): (arg: T) => voidWhen not to use
- Feeding state that components render. Push into a
Subjectand read derived streams withuseObservableoruseSyncObservableinstead. The flow stays visible and testable. - Anything another stream needs to compose with. The internal subject is unreachable from outside the hook.
- New code, as a default. Reach for the explicit pattern first.
Example, with the explicit-Subject equivalent to prefer:
import {useMemo, useState} from 'react'
import {useObservable, useObservableEvent} from 'react-rx'
import {scan, Subject, tap} from 'rxjs'
// With useObservableEvent: the pipeline is subscribed invisibly, for side effects
function WithHook() {
const [count, setCount] = useState(0)
const handleClick = useObservableEvent((clicks$) =>
clicks$.pipe(
scan((count) => count + 1, 0),
tap(setCount),
),
)
return <button onClick={handleClick}>Clicked {count} times</button>
}
// Preferred: events push into a Subject; hooks read the derived stream
function WithSubject() {
const [clicks$] = useState(() => new Subject<void>())
const count = useObservable(
useMemo(() => clicks$.pipe(scan((count) => count + 1, 0)), [clicks$]),
0,
)
return <button onClick={() => clicks$.next()}>Clicked {count} times</button>
}