Skip to Content
Migratev4 to v5

Migrating from v4 to v5

v5 is a major release. This page covers every breaking change and the recommended upgrade path for the headline hook change.

Requirements

v4v5
React18+^19.2
RxJS7.x (operators often from 'rxjs/operators')^7.2, import operators from 'rxjs'
Node(unspecified)>=22.12
Module formatCJS + ESMESM-only

See the RxJS import migration guide  if you still import from 'rxjs/operators'.

Deferred useObservable (headline change)

In v4, useObservable was built on useSyncExternalStore and forced synchronous React updates. Under load that blocks the main thread for chrome that does not need to be sync — validation, presence, previews, permissions, and similar.

v5 makes that the library default:

  • useObservable — store updates are deferred with useDeferredValue. Urgent renders keep the previous value; a background render catches up. Mounts, remounts, and <Activity> reveals still show the live snapshot (no initial-value flash).
  • useSyncObservable — exact v4 synchronous behavior, including the strict server snapshot (initialValue on the server, throws without one).

What you may notice

  • Controlled inputs can lag or lose caret position under load if they keep using useObservable — switch those reads to useSyncObservable.
  • The rendered value can briefly trail the store, so imperative reads or equality checks against it can observe a stale value. Keep a sync read for write-path equality when needed.
  • Render-count / test assertions may see extra passes: one Object.is bail-out pass on mount when the snapshot is defined, and an urgent-plus-deferred pair per emission.
  • SSR now renders synchronous emissions instead of the initialValue, and no longer throws when initialValue is omitted. Non-deterministic sync emissions can surface hydration mismatches that were previously masked. Synchronously erroring observables now fail the server render instead of exploding at hydration.

Keep calling useObservable everywhere. Switch only controlled-input (or same-event synchronous) reads to useSyncObservable:

// Before (v4) const text = useObservable(text$, '') const items = useObservable(items$, []) // After (v5) — only the input value needs to be sync const text = useSyncObservable(text$, '') const items = useObservable(items$, [])

Also remove redundant wrappers that are now built in:

// Before const results = useDeferredValue(useObservable(results$)) // After const results = useObservable(results$)

Fallback: mechanical rename

If you need a zero-behavior-change upgrade day, rename every useObservableuseSyncObservable, then adopt deferral incrementally by switching non-input reads back to useObservable.

See it in action

The Suspense & deferred values example puts both hooks side by side on a search UI.

Last updated on