Values like navigator.onLine or a third-party store's state change outside React's control, and manually syncing them with useState and useEffect can 'tear' under concurrent rendering. useSyncExternalStore solves this with a subscribe/getSnapshot contract. This lesson covers how it works and where you'll actually encounter it.
1State That Lives Outside React
Not every value a component depends on is React state ā window.innerWidth, navigator.onLine, and third-party state library stores all change independently of any setState call, living in what's called an external store.
2The Naive Approach Has a Tearing Bug
A manual useState plus useEffect subscription to an external value's change events mostly works, but under React 18's concurrent rendering it can produce tearing ā different parts of the UI briefly rendering inconsistent values of the same external store within a single update.
3useSyncExternalStore's Contract
useSyncExternalStore accepts a subscribe function, which registers a callback to be notified of store changes, and a getSnapshot function, which returns the store's current value. React uses this contract to read the external store safely and consistently, even during concurrent rendering, fully eliminating tearing.
4getSnapshot Must Return a Stable Reference
getSnapshot must return the exact same reference (for objects and arrays) when the underlying value hasn't actually changed. Returning a freshly created object on every call, even with identical contents, causes React to believe the store changed on every render, resulting in an infinite re-render loop.
5Where You'll Actually Encounter This
Most developers never call useSyncExternalStore directly ā it's the mechanism state management libraries like Redux, Zustand, and Jotai use internally to safely connect their external stores to React components without tearing under concurrent rendering.
6Step-by-Step Breakdown
State That Lives Outside React. Not all state your components care about is managed by React. window.innerWidth, a browser's online/offline status, a third-party state library, or a value shared across multiple React roots on the same page all live in an 'external store' that changes on its own, outside of any setState call.
The Naive Approach Has a Tearing Bug. You might try useState + useEffect with a manual event listener to sync external state. This mostly works, but under React 18's concurrent rendering, it can produce 'tearing' ā different parts of the UI briefly showing inconsistent values of the same external store during a single render pass.
What subtle bug can a manual useState + useEffect subscription to external state suffer from under React's concurrent rendering?
- āTearing ā different parts of the UI briefly showing inconsistent values
- āIt's guaranteed to leak memory in every case
useSyncExternalStore's Contract. useSyncExternalStore takes two functions: a subscribe function that registers a callback to be notified of changes, and a getSnapshot function that returns the current value. React uses these to read the store safely and consistently, even during concurrent rendering, eliminating the tearing problem entirely.
getSnapshot Must Return a Stable Reference. getSnapshot should return the exact same value (by reference, for objects/arrays) if nothing has actually changed. Returning a brand-new object every call ā even with identical contents ā tricks React into thinking the store changed on every render, causing an infinite re-render loop.
Why must getSnapshot return the same reference when the underlying value hasn't changed?
- āA new reference every call makes React think the store changed, causing an infinite render loop
- āIt has no real effect ā it's purely a style preference
Where You'll Actually Encounter This. Most React developers never call useSyncExternalStore directly ā state management libraries like Redux, Zustand, and Jotai use it internally to safely connect their external stores to React. Understanding it helps you know why those libraries are tear-free under concurrent rendering, and lets you build your own safe external-store hook when you need one.
Mastery Achieved. You now understand useSyncExternalStore: why manually syncing external state with useState/useEffect can tear under concurrent rendering, how subscribe and getSnapshot solve that safely, why getSnapshot must return a stable reference, and where you'll actually encounter this hook in practice ā mostly inside the state libraries you already use. Next, you'll learn the rare, low-level useInsertionEffect hook.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported since React 18.
Fully supported since React 18.
Fully supported since React 18.
Fully supported since React 18.
Accessibility (A11y)
1Tearing-Free State Prevents Inconsistent Announced Content
If two parts of the page announce inconsistent live status (like one saying 'online' and another 'offline' from the same underlying value), it directly confuses assistive technology users ā useSyncExternalStore's consistency guarantee prevents exactly this class of bug.
SEO Implications
- 1
useSyncExternalStore Is a Client-Only Concern
It addresses consistency during client-side concurrent rendering and has no bearing on server-rendered HTML content, though it does accept an optional third argument for a server snapshot to support SSR use cases correctly.
Best Practices
Keep getSnapshot Cheap and Synchronous
getSnapshot may be called frequently by React during rendering ā avoid expensive computation inside it, and never perform async work, since it must return a value synchronously.
Prefer an Established State Library Over Hand-Rolling useSyncExternalStore
Unless building a genuinely custom external-store integration, reach for a library like Zustand that already implements this pattern correctly, rather than re-deriving subscribe/getSnapshot logic from scratch.
Frequent Bugs
A component using useSyncExternalStore enters an infinite render loop.
getSnapshot is returning a newly created object or array on every call instead of a stable reference. Return a primitive value, or cache and reuse the same object reference until the underlying value genuinely changes.
Two sibling components briefly show different values for what should be the same piece of external state.
This is the tearing bug that a manual useState + useEffect subscription is prone to under concurrent rendering. Replace it with useSyncExternalStore, which guarantees a consistent value is read across a single render pass.
Real-World Examples
A Tear-Free Online/Offline Status Hook
An app needs multiple components to reliably agree on whether the browser is currently online, even during concurrent rendering. Implementing useOnlineStatus with useSyncExternalStore, backed by the browser's online/offline events and navigator.onLine, guarantees every component reading it sees a consistent value within the same render.
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot);
}