useSyncExternalStore
useSyncExternalStore is a React Hook designed for reading and subscribing to data that lives outside of React — browser APIs like navigator.onLine, third-party state libraries, or your own hand-rolled stores. It exists because concurrent rendering can pause, resume, and discard work, and naive useEffect-based subscriptions can show inconsistent (torn) values during that process. This Hook gives React enough information to keep every component in sync with the source of truth on every render.
Why external stores need a dedicated Hook
Before this Hook, a common pattern was to read a value into state and update it from an effect. That works in synchronous React, but concurrent features such as startTransition and useDeferredValue allow React to render in the background and interrupt that work. If the external value changes mid-render, different components can read different versions of it — a glitch known as tearing. useSyncExternalStore forces a consistent read by re-checking the snapshot synchronously before committing, so the UI never displays a mismatched mixture of old and new data.
The API and its three arguments
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
| Argument | Type | Purpose |
|---|---|---|
subscribe | (callback) => unsubscribe | Registers a callback that the store calls whenever its data changes. Returns a cleanup function. |
getSnapshot | () => value | Returns the current value of the store. Must return a cached, referentially stable value when nothing changed. |
getServerSnapshot | () => value | Optional. Returns the initial value during server-side rendering and hydration. |
Two rules make or break this Hook. First, getSnapshot must return the same reference when the data has not changed — returning a fresh object/array on every call causes an infinite render loop. Second, subscribe should be a stable function (defined outside the component or memoized), otherwise React re-subscribes on every render.
Returning
{ ... }or[ ... ]directly fromgetSnapshotis the most common mistake. Cache the value and compare it yourself, or store primitives.
A complete useOnlineStatus example
The browser exposes connectivity through navigator.onLine and the online / offline window events. This is a textbook external store: a value plus an event to subscribe to.
import { useSyncExternalStore } from "react";
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 getServerSnapshot() {
return true; // assume online on the server
}
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
Because getSnapshot returns a boolean primitive, identity is never a concern. Now any component can consume it:
import { useOnlineStatus } from "./useOnlineStatus";
export default function StatusBar() {
const isOnline = useOnlineStatus();
return (
<p>{isOnline ? "✅ Connected" : "❌ Offline — changes will sync later"}</p>
);
}
Toggling your network in devtools updates every component using the Hook at once:
Output:
✅ Connected // network restored
❌ Offline — changes will sync later // network dropped
Building a custom store
For non-primitive data you control the store yourself. The pattern is a tiny pub/sub object with getSnapshot, subscribe, and mutation methods. Keep a cached snapshot so its reference only changes when the data actually changes.
function createStore(initialState) {
let state = initialState;
const listeners = new Set();
return {
getSnapshot() {
return state;
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
setState(updater) {
const next = typeof updater === "function" ? updater(state) : updater;
if (Object.is(next, state)) return; // skip no-op updates
state = next;
listeners.forEach((l) => l());
},
};
}
export const cartStore = createStore({ items: [], total: 0 });
import { useSyncExternalStore } from "react";
import { cartStore } from "./cartStore";
export function useCartTotal() {
return useSyncExternalStore(
cartStore.subscribe,
() => cartStore.getSnapshot().total
);
}
Note that the getSnapshot here returns state.total — a number. Returning a derived primitive sidesteps the identity trap entirely. When you genuinely need an object, only replace state with a new reference inside setState, never inside getSnapshot.
Selecting a slice of state
To subscribe to part of a larger store, derive the slice inside getSnapshot and make sure unchanged slices keep their reference. For object slices, the community helper useSyncExternalStoreWithSelector (from use-sync-external-store/with-selector) accepts a selector plus an equality function, which is what most state libraries use under the hood.
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
export function useCartItemCount() {
return useSyncExternalStoreWithSelector(
cartStore.subscribe,
cartStore.getSnapshot,
cartStore.getSnapshot, // server snapshot
(state) => state.items.length // selector returns a primitive
);
}
Best Practices
- Reach for this Hook only for external data; for state owned by React, use
useStateoruseReducer. - Define
subscribeoutside your component (or wrap it inuseCallback) so React does not re-subscribe on every render. - Make
getSnapshotreturn a referentially stable value — prefer primitives, or cache the object and update it only on real changes. - Always provide
getServerSnapshotif your app uses server rendering, or hydration will throw a mismatch warning. - Wrap the Hook in a custom Hook (
useOnlineStatus,useCartTotal) so the subscription wiring stays out of your components. - For large stores, use a selector via
useSyncExternalStoreWithSelectorto avoid re-rendering on unrelated changes.