Subscribe to State Outside of React
useSyncExternalStore
When state lives outside React — in a global store, a browser API, or a third-party library — `useSyncExternalStore` is the safe way to read and re-render on changes.
What you'll learn
- Subscribe to an external state source
- Avoid race conditions with concurrent rendering
Most state lives inside React (useState, useReducer). But
sometimes you need to subscribe to external state — a global
store like Redux/Zustand, a browser API like window.matchMedia,
or anything that isn’t React.
useSyncExternalStore(subscribe, getSnapshot) is the official hook
for that.
The Shape
const value = useSyncExternalStore(
subscribe, // (listener) => unsubscribe
getSnapshot, // () => current value
); subscribe is called once. It should attach a listener and return
a function to detach it. Whenever the listener is called, React
calls getSnapshot and re-renders if the value changed.
Example: Online Status
function useOnlineStatus() {
return useSyncExternalStore(
listener => {
window.addEventListener("online", listener);
window.addEventListener("offline", listener);
return () => {
window.removeEventListener("online", listener);
window.removeEventListener("offline", listener);
};
},
() => navigator.onLine
);
} getSnapshot returns the current value (a boolean here). When
navigator.onLine changes, the event listeners fire, React calls
getSnapshot, sees a new value, re-renders.
Why Not Just useEffect + useState?
You can write a similar hook with useState + useEffect. But:
- It can tear during concurrent renders (different parts of a render see different values)
- You have to wire up the initial sync yourself
useSyncExternalStore is the React-team-recommended primitive for
“safely subscribe to external state”. State libraries (Zustand,
Redux Toolkit) use it internally.
Returning Objects
Each call to getSnapshot should return the same reference if
the value hasn’t changed. Returning a new object every time will
make React think the state changed every render and re-render in
a loop:
// ✗ New object every snapshot → infinite re-renders
() => ({ width: window.innerWidth })
// ✓ Memoize, or return a primitive
() => window.innerWidth You’ll Use It Through Libraries
In day-to-day code, you’ll rarely call useSyncExternalStore
directly. You’ll use a state library’s hook (useStore in
Zustand, useSelector in Redux) — those wrap
useSyncExternalStore for you.
It’s still good to know what’s underneath.
Up Next
A newer hook for separating the “non-reactive” parts of an effect.
useEffectEvent →