React.js, part 6: useSyncExternalStore — connecting React to the world outside it
Part 6from the React.js series · 20 parts in all
Sooner or later a component must render data that React does not own: a browser API, a
WebSocket, a global store, matchMedia, navigator.onLine. The old
answer — subscribe in an effect and mirror into state — has a well-known flaw: it can tear,
and it renders one frame of stale data between subscribe and effect. React 18's answer is
useSyncExternalStore.
The hook's contract
You give React three things: a subscribe function, a getSnapshot that returns the current value, and an optional server-side snapshot:
import { useSyncExternalStore } from 'react';
function useOnline() {
return useSyncExternalStore(
// subscribe: tell the store to notify us, return the unsubscribe
(onChange) => {
window.addEventListener('online', onChange);
window.addEventListener('offline', onChange);
return () => {
window.removeEventListener('online', onChange);
window.removeEventListener('offline', onChange);
};
},
// getSnapshot: the CURRENT value - must be cheap and stable
() => navigator.onLine,
// getServerSnapshot: what the server should render
() => true
);
}
export function Status() {
return <span>{useOnline() ? 'online' : 'offline'}</span>;
}
React subscribes during render — before paint — so there is no stale frame, and it detects store changes that happened during a concurrent render so two parts of the tree can never disagree. That last property is the "sync" in the name.
The one rule that bites people
getSnapshot must return a value that is referentially stable
when nothing changed. Returning a fresh object every call makes React think the store
changes on every render and loop forever. For an object store, cache the snapshot:
let cached = store.getState();
function getSnapshot() {
return cached; // same reference until the store actually changes
}
store.subscribe(() => { cached = store.getState(); });
When to reach for it
Not for ordinary app state — that is useState and reducers. It is for
external state: third-party stores (Redux and Zustand both use this internally),
browser globals, and subscriptions. With it, "subscribe in an effect" becomes a
correctness smell rather than a default. Next: two quiet hooks —
useDeferredValue and useId — that fix real, everyday bugs.