Skip to Content
Guide

Getting Started

Installation

npm i react-rx rxjs

Observable Hooks

useObservable()

Use observables in React components with the useObservable hook.

If you need to subscribe to an observable in your component, this hook will give you the current value from it

Example:

import {useMemo} from 'react' import {useObservable} from 'react-rx' import {interval} from 'rxjs' function MyComponent(props) { const observable = useMemo(() => interval(100), []) const number = useObservable(observable, 0) return <>The number is {number}</> }

The initialValue argument is optional. If its omitted, the value returned from useObservable may be null initially. If the observable emits a value synchronously at subscription time, that value will be used as the initial value, and any initialValue passed as argument to useObservable will be ignored:

import {useMemo} from 'react' import {useObservable} from 'react-rx' import {of} from 'rxjs' // This component will never render "Hello mars!" since the observable emits "world" synchronously. function MyComponent(props) { const observable = useMemo(() => of('world'), []) const planet = useObservable(observable, 'mars') return <>Hello {planet}!</> }

The disabled option pauses the hook’s active subscription — think of it like pause: true. While disabled is true, the hook will not keep a live subscription that pushes updates into the component, and it returns the last value it already received (or the initialValue if nothing has been received yet). Turning disabled back to false resumes the live subscription.

Important: disabled does not skip the hook’s initial warm-up subscription. useObservable always briefly subscribes during render so a synchronous emission can become the current snapshot. That means cold observables with subscribe-time side effects (for example fromFetch) still run that work even when disabled is true.

import {useEffect, useState} from 'react' import {useObservable} from 'react-rx' import {Subject} from 'rxjs' // While `disabled` is true, later async emissions are ignored and the last // received value (here the initialValue "mars") is returned. function MyComponent(props) { const [observable] = useState(() => new Subject<string>()) const planet = useObservable(observable, 'mars', {disabled: true}) useEffect(() => { observable.next('world') }, [observable]) return <>Hello {planet}!</> }

If the goal is to avoid any subscription to a particular observable, do not use disabled. Pass a different observable instead — for example swap in of(null) until you are ready to fetch:

import {useMemo} from 'react' import {useObservable} from 'react-rx' import {of} from 'rxjs' import {fromFetch} from 'rxjs/fetch' function Users({shouldFetch}: {shouldFetch: boolean}) { // Prefer swapping the observable over `{disabled: !shouldFetch}`: // `disabled` still performs the render-phase warm-up subscribe, which would // fire the request even when `shouldFetch` is false. const users$ = useMemo( () => shouldFetch ? fromFetch('https://api.github.com/users?per_page=5', { selector: (response) => response.json(), }) : of(null), [shouldFetch], ) const users = useObservable(users$, null) return <pre>{JSON.stringify(users, null, 2)}</pre> }

Because the fetch observable is only created (and therefore only ever subscribed) when shouldFetch is true, this guarantees zero subscriptions to fromFetch until then.

useObservablePromise()

Use this when you want Suspense-powered data fetching instead of tracking loading state in the stream.

useObservable is built on useSyncExternalStore. That is great for live values, but it cannot activate a Suspense boundary, and React 19.2 Activity pre-rendering only fetches data read with use(promise).

useObservablePromise returns an instrumented Promise you pass to React’s use(). The hook itself does not suspend — the consumer decides where the Suspense boundary lives:

import {Suspense, use, useMemo} from 'react' import {useObservablePromise} from 'react-rx' import {fromFetch} from 'rxjs/fetch' function Users() { const users$ = useMemo( () => fromFetch('https://api.github.com/users?per_page=5', { selector: (response) => response.json(), }), [], ) const promise = useObservablePromise(users$) return ( <Suspense fallback={<p>Loading users…</p>}> <UsersList promise={promise} /> </Suspense> ) } function UsersList({promise}: {promise: Promise<unknown>}) { const users = use(promise) return <pre>{JSON.stringify(users, null, 2)}</pre> }

Prefer creating the promise in a parent that does not suspend (as above), so Suspense retries always see the same promise identity. The single-component form also works when the observable identity is stable across retries (module-level cache, or a shared WeakMap keyed by request):

function UsersList({users$}) { // `users$` must be referentially stable for the in-flight request const users = use(useObservablePromise(users$)) return <pre>{JSON.stringify(users, null, 2)}</pre> }

Semantics

  • Suspends until the observable’s first emission (firstValueFrom semantics).
  • Later emissions update the UI without re-showing the Suspense fallback.
  • Sync sources (of, BehaviorSubject, replayed shareReplay) never flash a fallback.
  • Errors reject the promise and surface through the nearest Error Boundary. Prefer catchError on the inner observable when you want graceful degradation instead of a boundary.
  • Completing without emitting rejects with RxJS EmptyError.

Not for startWith placeholders. Because the first emission unblocks Suspense, startWith('loading') fulfills immediately with "loading". For placeholder / loading-value patterns, use useObservable instead.

Options

useObservablePromise(observable$, { disabled?: boolean // default false — when true, this component starts no fetch ttl?: number // default 500 — retention (ms) after settle with no subscribers })

Unlike useObservable’s disabled (which still runs a warm-up probe), disabled: true here fully prevents fetching on behalf of this component. The returned promise is still the shared cache entry — a sibling or preloadObservablePromise can warm it.

ttl controls how long a settled value stays reusable after unmount. Remount within the window reuses the promise (no refetch, no fallback). After it expires, the next mount refetches. Eviction only affects future consumers: components that are still mounted keep their value — hiding an <Activity> tree longer than ttl never drops what it already rendered.

Deferring expensive re-renders

Like every external-store subscription, emission-driven updates render at synchronous priority — React cannot time-slice them directly. If an emission re-renders something expensive, defer the promise itself and memoize the expensive subtree. The synchronous pass then skips the memoized subtree (it still sees the old promise), and its re-render happens at deferred priority: time-sliced, interruptible by urgent updates, and coalesced under rapid emissions.

const BigChart = memo(function BigChart({promise}) { const data = use(promise) return <Chart data={data} /> }) function Dashboard({metrics$}) { const promise = useObservablePromise(metrics$) const deferredPromise = useDeferredValue(promise) return <BigChart promise={deferredPromise} /> }

The memo is load-bearing: without it the subtree re-renders during the synchronous pass anyway (with the old promise), defeating the deferral — see deferring re-rendering for a part of the UI . Swapped promises are always pre-settled, so the deferred subtree never suspends — it just lags by a paint under load. When the stream itself is too chatty, throttling in the pipe (auditTime, throttleTime) remains the RxJS-native complement.

Preloading

Warm the cache outside of render (hover, route loaders) with preloadObservablePromise:

import {preloadObservablePromise, useObservablePromise} from 'react-rx' function TabButton({users$, onSelect}) { return ( <button type="button" onMouseEnter={() => preloadObservablePromise(users$, {ttl: 5_000})} onClick={onSelect} > Users </button> ) }

Which hook when?

NeedHook
Live values, timers, subjects, optional initialValueuseObservable
Async data + Suspense / Activity pre-renderuseObservablePromise
Event → observable pipelinesuseObservableEvent

For cold observables you want to share across subscribers yourself, keep using RxJS shareReplay({bufferSize: 1, refCount: true}) — the hook’s ttl is a lightweight mount/unmount cache, not a full query cache.

useObservableEvent()

This creates an event handler that can be used to create an observable from events.

Here’s an example of a component that displays the current value from a range input:

import {useState} from 'react' import {useObservableEvent} from 'react-rx' import {filter, map, tap} from 'rxjs' const ShowSliderValue = () => { const [value, setValue] = useState(1) const handleChange = useObservableEvent((value$) => value$.pipe( // Ignore nullish values filter(nonNullable), // Cast to number map((value) => Number(value)), // Update local state tap(setValue), ), ) return ( <> <input type="range" value={value} onChange={(event) => handleChange(event.target.value)} min={1} max={10} /> <div>Value is: {value}</div> </> ) } function nonNullable<T>(v: T): v is NonNullable<T> { return v != null }
Last updated on