useSyncExternalStore
Subscribes to an external store and reads a snapshot of its data. The recommended way to integrate non-React state management with React's concurrent features.
Signature
const snapshot = useSyncExternalStore<T>(subscribe: (cb: () => void) => () => void, getSnapshot: () => T, getServerSnapshot?: () => T)Parameters
| Parameter | Type | Description |
|---|---|---|
| subscribe | (callback: () => void) => () => void | A function that subscribes to the store. Receives a callback to call when the store changes. Returns an unsubscribe function. |
| getSnapshot | () => T | A function that returns the current snapshot of the store data. Must return the same value (by Object.is) if the store hasn't changed. |
| getServerSnapshot | () => T | Optional function returning the snapshot for server rendering. Required for SSR compatibility. |
Return Value
The current snapshot of the store, as returned by getSnapshot. React re-renders the component when the snapshot changes.
Examples
import { useSyncExternalStore } from 'react';
function subscribe(callback: () => void) {
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 during SSR
}
function OnlineIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return <span>{isOnline ? '🟢 Online' : '🔴 Offline'}</span>;
}import { useSyncExternalStore } from 'react';
function createStore<T>(initialState: T) {
let state = initialState;
const listeners = new Set<() => void>();
return {
getSnapshot: () => state,
subscribe: (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
setState: (updater: (prev: T) => T) => {
state = updater(state);
listeners.forEach(l => l());
},
};
}
const counterStore = createStore({ count: 0 });
function Counter() {
const { count } = useSyncExternalStore(
counterStore.subscribe,
counterStore.getSnapshot,
);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => counterStore.setState(s => ({ count: s.count + 1 }))}>+</button>
</div>
);
}import { useSyncExternalStore } from 'react';
let dimensions = { width: 0, height: 0 };
function subscribeDimensions(callback: () => void) {
const update = () => {
dimensions = { width: window.innerWidth, height: window.innerHeight };
callback();
};
update();
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}
function getDimensionsSnapshot() {
return dimensions;
}
function getServerDimensions() {
return { width: 1024, height: 768 };
}
function useWindowDimensions() {
return useSyncExternalStore(
subscribeDimensions,
getDimensionsSnapshot,
getServerDimensions,
);
}Common Pitfalls
Creating new subscribe or getSnapshot functions on every render — define them outside the component or memoize them.
Returning a new object from getSnapshot on every call — this causes infinite re-renders. Return the same reference when the data hasn't changed.
Not providing getServerSnapshot for SSR, which causes hydration errors.
Using useSyncExternalStore for React state — it's designed for external (non-React) stores only.
Understanding useSyncExternalStore
useSyncExternalStore is the official way to subscribe to external data sources in React. It was introduced to solve the "tearing" problem in concurrent rendering, where different parts of the UI could show inconsistent data if they read from an external store at different times during a render. This hook guarantees that all components reading from the same store see a consistent snapshot.
The subscribe function tells React how to listen for store changes. It receives a callback that React calls when it needs to check for updates. The function must return an unsubscribe cleanup function. Crucially, the subscribe function should be stable — either defined outside the component or memoized — to avoid unnecessary resubscriptions on every render.
The getSnapshot function must follow strict rules: it must return the exact same value (by Object.is comparison) if the store hasn't changed. Returning a new object or array on every call, even with identical contents, will cause an infinite re-render loop because React sees each call as returning a "new" value. For stores containing objects, cache the last returned value and only create a new reference when the data actually changes.
This hook is the foundation that state management libraries like Zustand, Jotai, and Valtio use internally to integrate with React's concurrent features. While you can build your own store with useSyncExternalStore, most applications benefit from using an established library that handles edge cases like selector optimization, middleware, and DevTools integration. The hook is most useful when integrating with non-React data sources like browser APIs, legacy stores, or shared workers.
Related Hooks
useStateDeclares a state variable that triggers re-renders when updated. The most fundamental React hook for managing component-level state.
useEffectSynchronizes a component with an external system by running side effects after render. Handles subscriptions, data fetching, DOM manipulation, and cleanup.
useContextReads and subscribes to a context value. Re-renders the component whenever the nearest provider's value changes.
More Other Hooks
Explore All React Hooks
Browse our complete reference of 19 React hooks with signatures, examples, pitfalls, and in-depth explanations.