Skip to Content
Comparison

Comparison

Two different questions get asked about react-rx. “Why this instead of another RxJS binding?” And “why streams instead of my state library?” They deserve different answers.

RxJS ↔ React bindings

You already have observables. The bindings differ in update timing, Suspense integration, and how much ceremony each stream needs:

Capabilityreact-rx observable-hooks @react-rxjs/core DIY useEffect
Deferred, identity-coherent updatesBuilt into useObservableNo (synchronous)No (synchronous)Manual
Suspense data fetchinguse()-compatible promise; streams update in place without re-suspendingObservableResourceSuspends until first valueManual
<Activity> pre-rendering (React 19.2)Fetches start during render; preloadObservablePromise for hover warm-upNoNoNo
Sync emission on first paintNo extra re-renderuseObservableEagerStateWith an active subscriptionExtra render after mount
Setup per streamNone; pass any ObservableNonebind() plus <Subscribe> or an active subscriptionSubscription plumbing each time
SSRNever throws; server matches the client’s first paintRenders initial stateNoManual
React CompilerTest suite runs through itNoNoNot applicable
React / RxJS supportReact 19.2+, RxJS 7.2+React 16.8+, RxJS 6 and 7React 16.8+, RxJS 7+Any

The honest flip side: observable-hooks and @react-rxjs support much older React versions. react-rx targets the newest React on purpose. The concurrent-rendering rows above are the reason.

Want to feel the differences? Try the Suspense demo and the Activity demo.

General state libraries

Zustand, Jotai, XState, and TanStack Query solve different shapes of state. All of them are good at their shape. The question that matters: which shape does your state have?

react-rx + RxJSZustand Jotai XState TanStack Query 
Mental modelStreams of values over timeOne plain storeAtomsState machines and actorsServer-state cache
Live, multi-emission data (sockets, tokens, presence)First-classManual set() callsManual writesObservable/callback actorsPolling or manual cache writes
Cancellation and race handlingswitchMap / exhaustMap semanticsManualAbortSignal in async atomsStop/restart actors on transitionCancels and dedupes per query key
Time (debounce, intervals, “x seconds ago”)Operators: timer, debounceTime, auditTimeManualManualDelayed transitionsstaleTime / refetchInterval
Combining several async sourcescombineLatest / merge / zipManualDerived async atomsMachine orchestrationDependent queries
Request caching and invalidationCompose it yourself (shareReplay, ttl)NoAtom-levelNoBest-in-class: invalidation, dedupe, persistence
Offline and retryretry / repeat you composeNoNoNoBuilt-in retries, refetch-on-reconnect, offline mutation queue
Explicit workflow modelingEncoded in stream compositionNoNoStatecharts, the whole pointNo
SuspenseuseObservablePromise + use()NoAsync atoms suspendManual (promise actors)useSuspenseQuery
Usable outside ReactRxJS runs anywhereVanilla storecreateStoreActors run anywhereFramework-agnostic core

When to reach for which

  • react-rx + RxJS when state is live, multi-value, or time-based. Streaming tokens, sockets, presence, timers, resumable polling. Anything where “the value” is really a sequence of values with cancellation and timing rules.
  • TanStack Query when state is a cache of request/response pairs. You get invalidation, dedupe, and offline mutations without building them.
  • XState when the hard part is which states exist and which transitions are legal. Checkout flows, wizards, connection lifecycles.
  • Zustand / Jotai when you need shared client state with minimal ceremony and no streaming semantics.

They compose

These are not either/or choices:

  • XState v5 accepts observables as actor input (fromObservable). A react-rx codebase can drive machines from the same streams components read.
  • A TanStack Query cache can own request/response state while react-rx owns the live layers on top (subscriptions, streaming updates). A stream can also write into the query cache.
  • Any external store (Zustand included) can be exposed as an observable and read through useObservable, next to the rest of your streams.
Last updated on