useSyncExternalStore for tearing-free external store subscriptions
Published July 19, 2026
import { useSyncExternalStore } from "react";
const EVENTS = ["online", "offline"] as const;
const subscribe = (onStoreChange: () => void) => {
EVENTS.forEach((event) => window.addEventListener(event, onStoreChange));
return () => EVENTS.forEach((event) => window.removeEventListener(event, onStoreChange));
};
const getSnapshot = () => navigator.onLine;
const getServerSnapshot = () => true;
export const useOnlineStatus = () =>
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);What
useSyncExternalStore lets a component read state that lives outside React — a browser API, a Redux-style store, a WebSocket connection — and re-render whenever that state changes, without ever showing a torn (inconsistent) snapshot across components during a concurrent render.
Why it matters
Before this hook, subscribing to external state meant useEffect + useState: subscribe in an effect, call setState on every change. That works for one component in isolation, but under React's concurrent rendering, two components reading the same external store at slightly different points in a single render pass can each commit a different snapshot of it — a "tear." useSyncExternalStore exists specifically to close that gap: React calls getSnapshot() during render and again after committing, forcing a synchronous re-render if the store changed in between.
navigator.onLine is a textbook case. It's real mutable state that lives on window, not in React state, and several components — a connectivity banner, a sync indicator, a disabled submit button — might all need to agree on its value at the same instant.
How it works
The snippet above breaks into the three arguments useSyncExternalStore takes:
subscriberegisters a change listener and returns an unsubscribe function. React calls it once per mounted component and cleans it up automatically — no manualuseEffectneeded.getSnapshotreturns the current value. React calls it on every render (and again after every store change) to decide whether a re-render is needed. It must return a value that'sObject.is-stable when nothing actually changed, or you'll get a render loop.getServerSnapshotcovers server rendering, wherenavigatordoesn't exist. Returningtrue(assume online) avoids a hydration crash; the client re-checks the real value on mount.
Gotchas
getSnapshotruns on every render, including renders React later discards — keep it cheap and synchronous.- Don't construct a new object or array inside
getSnapshotwhen the underlying value hasn't changed. That breaks theObject.ischeck and causes a re-render storm.navigator.onLineis a primitive boolean, so it's not an issue here, but it's the first thing to check with richer external stores. - This hook is for state that genuinely lives outside React. If it already lives in
useState,useReducer, or context, you don't need it.
Related
useTransition— keeps React-owned state responsive under load; a different tool for a related "avoid janky UI" goal.useDeferredValue— same goal asuseTransition, applied to a value instead of a transition.