I once read the theme from Redux, clicked the toggle, watched the store update — and the screen still showed the old theme. Refreshed. Fixed. Classic. I assumed React was broken. It wasn't. We were just reading from the wrong place at the wrong time.
React 18 brought concurrent rendering. Sounds fancy. In practice it means React can pause a render, do something else, then pick up where it left off. Great for performance. But here's the catch: if you read from an external store mid-render, the UI can tear — one part of the screen on old data, another on new. The app looks drunk. The user didn't do anything wrong.
loading demo…
what we used to do
Redux, Zustand, window.innerWidth, localStorage — they all live outside React. React has no idea when they change. The old pattern was to subscribe in useEffect:
function ThemeBadge() {
const [theme, setTheme] = useState(store.getState().theme);
useEffect(() => {
return store.subscribe(() => {
setTheme(store.getState().theme);
});
}, []);
return <span>{theme}</span>;
}It works. Mostly. But three quiet problems stack up. First — the initial render can be stale on SSR. Second — you pay for an extra render after every subscribe callback. Third — and this is the big one — in concurrent mode, if you read store.getState() directly during render, React can't guarantee the whole tree sees the same snapshot.
what React asked for
The team wanted a contract from libraries: give me subscribe(callback) and getSnapshot(). I'll only read the snapshot during render. Store updated? I'll re-render. On the server? Give me getServerSnapshot() so hydration doesn't scream mismatch.
It started as useMutableSource — beta, controversial, the works. React 18 shipped it as useSyncExternalStore. The API is small. The reason behind it isn't: pull external state inside React's rendering guarantees.
React calls getSnapshot()
the whole tree sees the same version
subscribe callback fires
new snapshot, consistent UI
three arguments, one job
useSyncExternalStore(
subscribe, // (onStoreChange) => unsubscribe
getSnapshot, // () => state — must be immutable / cached
getServerSnapshot // optional, for SSR
)subscribe: the store says "hey, something changed." getSnapshot: React asks "what's the value now?" getServerSnapshot: what to show when building HTML on the server — otherwise the client yells mismatch the moment it hydrates.
libraries already use this
You might never write useSyncExternalStore by hand. It's behind useSelector in React-Redux. Behind useStore in Zustand. And it's a perfect fit for small custom hooks — like listening to the browser's online/offline events.
function subscribe(cb: () => void) {
window.addEventListener('online', cb);
window.addEventListener('offline', cb);
return () => {
window.removeEventListener('online', cb);
window.removeEventListener('offline', cb);
};
}
function getSnapshot() {
return navigator.onLine;
}
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, () => true);
}was it actually necessary?
If you're only on useState and useContext, you may never touch it. But the day you adopted Redux, or listened to window resize, or synced localStorage — you became a customer of external stores. Without React 18's hook, that pattern was technically unsafe under concurrent rendering.
useSyncExternalStore is the bridge React built so the ecosystem didn't have to rip itself apart. It fixed tearing. It cleaned up SSR. And it gave us a standard: subscribe, snapshot, done.
tl;dr
external store = outside React. concurrent render = can pause mid-flight. read mid-flight without a snapshot = UI can tear. useSyncExternalStore = subscribe + snapshot, the React-approved way. you're probably already using it — just not by name.