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
| v4 | v5 | |
|---|---|---|
| React | 18+ | ^19.2 |
| RxJS | 7.x (operators often from 'rxjs/operators') | ^7.2, import operators from 'rxjs' |
| Node | (unspecified) | >=22.12 |
| Module format | CJS + ESM | ESM-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 withuseDeferredValue. 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 (initialValueon 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 touseSyncObservable. - 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 wheninitialValueis 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.
Recommended: targeted migration
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 useObservable → useSyncObservable, 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.