useSyncExternalStore
Subscribe a component to any store outside React — safely, without tearing, and SSR-ready.
useSyncExternalStore is the official bridge between React and state that lives outside React: a Redux/Zustand store, a browser API like navigator.onLine or matchMedia, an event emitter, or a WebSocket. It is what state libraries use under the hood, and it is the correct way to read external mutable data during concurrent rendering.
Why not just useState + useEffect?
The naive approach — read the value in an effect and mirror it into state — can tear: during a concurrent render, different components can momentarily read different versions of the same source. useSyncExternalStore guarantees every component in a render sees one consistent snapshot.
The three arguments
- subscribe(callback) — register
callbackto run whenever the store changes; return an unsubscribe function. - getSnapshot() — return the current value. Must return a stable value (same reference for unchanged data), or you will loop.
- getServerSnapshot() (optional) — the value to use during server rendering / hydration.
const isOnline = useSyncExternalStore(
(cb) => {
window.addEventListener('online', cb);
window.addEventListener('offline', cb);
return () => {
window.removeEventListener('online', cb);
window.removeEventListener('offline', cb);
};
},
() => navigator.onLine, // client snapshot
() => true // server snapshot
);Wrap that in a useOnlineStatus custom hook and you have a clean, tear-free, SSR-safe primitive the rest of your app can reuse.
Example
import { useSyncExternalStore } from 'react';
// A minimal external store, fully outside React.
function createStore(initial) {
let state = initial;
const listeners = new Set();
return {
getState: () => state,
setState: (next) => {
state = typeof next === 'function' ? next(state) : next;
listeners.forEach((l) => l());
},
subscribe: (l) => {
listeners.add(l);
return () => listeners.delete(l);
},
};
}
const counterStore = createStore(0);
// A selector-style hook so components subscribe to just what they read.
function useCounter() {
return useSyncExternalStore(
counterStore.subscribe,
counterStore.getState,
() => 0 // server snapshot
);
}
function Display() {
const count = useCounter();
return <p>Count: {count}</p>;
}
function Controls() {
// Note: does NOT read the store, so it never re-renders on change.
return (
<button onClick={() => counterStore.setState((c) => c + 1)}>
Increment
</button>
);
}When to use it
- A Redux integration uses useSyncExternalStore internally to subscribe components to the Redux store so they re-render safely without tearing during concurrent renders.
- A browser online/offline indicator subscribes to window's 'online' and 'offline' events using useSyncExternalStore to give React a safe, tear-free read of navigator.onLine.
- A custom in-memory event bus shared between React and a legacy jQuery section uses useSyncExternalStore so React components stay in sync with events fired outside the tree.
More examples
Subscribe to navigator.onLine
Bridges the browser's online/offline events into React with useSyncExternalStore, including an SSR snapshot.
import { useSyncExternalStore } from 'react';
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
const getSnapshot = () => navigator.onLine;
const getServerSnapshot = () => true; // assume online on SSR
function NetworkStatus() {
const online = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return <p>{online ? 'Online' : 'Offline'}</p>;
}Subscribe to a custom store
Implements the minimal subscribe/getSnapshot interface that useSyncExternalStore requires from any external store.
// miniStore.js — a tiny observable store
let state = { count: 0 };
const listeners = new Set();
export const store = {
getSnapshot: () => state,
subscribe: (cb) => { listeners.add(cb); return () => listeners.delete(cb); },
increment: () => {
state = { count: state.count + 1 };
listeners.forEach(cb => cb());
},
};Use the custom store in a component
Connects the component to the custom store with useSyncExternalStore, which handles subscription and tear-free reads.
import { useSyncExternalStore } from 'react';
import { store } from './miniStore';
function Counter() {
const { count } = useSyncExternalStore(store.subscribe, store.getSnapshot);
return (
<>
<p>Count: {count}</p>
<button onClick={store.increment}>+</button>
</>
);
}
Discussion