Overview
useSyncExternalStore is a React hook for reading and subscribing to state that lives outside React. It gives React a consistent way to subscribe to an external source, read a snapshot during render, and update the component when that source changes.
External state can come from many places: a custom store, Redux-like store, browser API, WebSocket connection, media query, online/offline status, local storage event, or a third-party state library. The key requirement is that React needs a safe subscription contract.
This topic matters because subscribing to external state with a plain useEffect and useState can create subtle bugs in concurrent rendering, server rendering, hydration, and stale snapshot scenarios. useSyncExternalStore is the official low-level hook for building external store integrations.
For interviews, this topic tests whether a candidate understands the difference between React state and external state, how subscriptions work, why snapshots must be stable, and when application developers should use this hook directly versus relying on a state library.
Core Concepts
What Counts as External State
External state is state not owned by React's component state system.
Examples:
- Redux store.
- Zustand-like custom store.
- Browser online/offline status.
window.matchMediaresult.- WebSocket-backed presence store.
- Shared worker state.
- Local storage changes across tabs.
- A custom event emitter.
- A map library or editor model.
React can render from external state, but it needs to know when the state changes and how to read a consistent snapshot.
The useSyncExternalStore API
The hook shape is:
const snapshot = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot,
);
Arguments:
subscribe: registers a callback and returns an unsubscribe function.getSnapshot: returns the current client-side value.getServerSnapshot: optional function for server rendering and hydration.
Basic example:
function useOnlineStatus() {
return useSyncExternalStore(
subscribeToOnlineStatus,
() => navigator.onLine,
() => true,
);
}
function subscribeToOnlineStatus(callback: () => void) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
The component can now render based on a browser value that lives outside React.
subscribe
subscribe connects React to the external source.
function subscribe(callback: () => void) {
store.addListener(callback);
return () => store.removeListener(callback);
}
Requirements:
- It must call the callback when the external store changes.
- It must return a cleanup function.
- It should be stable when possible.
- It should not update React state directly.
- It should not perform rendering logic.
If subscribe is declared inside a component and changes on every render, React may resubscribe more often than needed.
getSnapshot
getSnapshot returns the current value from the external source.
function getSnapshot() {
return store.getState();
}
Important rule: repeated calls to getSnapshot must return the same value if the store has not changed.
Bad:
function getSnapshot() {
return { ...store.getState() };
}
This returns a new object every time, so React may think the snapshot changed even when the store did not.
Better:
function getSnapshot() {
return store.getState();
}
The store should maintain stable immutable snapshots or cache derived snapshots.
Simple Custom Store
Example external store:
type CounterStore = {
getSnapshot: () => number;
subscribe: (callback: () => void) => () => void;
increment: () => void;
};
function createCounterStore(): CounterStore {
let count = 0;
const listeners = new Set<() => void>();
return {
getSnapshot: () => count,
subscribe: (callback) => {
listeners.add(callback);
return () => listeners.delete(callback);
},
increment: () => {
count += 1;
listeners.forEach((listener) => listener());
},
};
}
export const counterStore = createCounterStore();
Hook:
function useCounterCount() {
return useSyncExternalStore(
counterStore.subscribe,
counterStore.getSnapshot,
counterStore.getSnapshot,
);
}
Component:
function Counter() {
const count = useCounterCount();
return (
<button onClick={counterStore.increment}>
Count: {count}
</button>
);
}
The store lives outside React. React subscribes and rerenders when the snapshot changes.
Why Not Just useEffect?
A basic subscription can be written with useEffect:
useEffect(() => {
return store.subscribe(() => {
setState(store.getState());
});
}, []);
This works for simple cases, but it has downsides:
- The first render may use stale or duplicated initial state.
- It is easier to mismatch render-time values and subscription updates.
- It is not the official contract for external stores.
- It is harder to support server rendering and hydration safely.
- It can be less robust with concurrent rendering.
useSyncExternalStore tells React exactly how to read the current snapshot during render and how to subscribe to future changes.
Snapshot Stability
Snapshot stability is the most common pitfall.
If getSnapshot returns a new object each time, React sees constant change.
Bad:
function getSnapshot() {
return {
todos: store.todos,
filter: store.filter,
};
}
Better:
let snapshot = {
todos: store.todos,
filter: store.filter,
};
function updateStore(nextTodos: Todo[], nextFilter: string) {
store.todos = nextTodos;
store.filter = nextFilter;
snapshot = {
todos: nextTodos,
filter: nextFilter,
};
emitChange();
}
function getSnapshot() {
return snapshot;
}
For mutable stores, cache immutable snapshots so unchanged state returns the same reference.
Selectors
Many components only need part of a store.
Conceptual selector:
function useUserName() {
const state = useSyncExternalStore(
userStore.subscribe,
userStore.getSnapshot,
userStore.getSnapshot,
);
return state.user.name;
}
This works, but the component may rerender whenever the full snapshot changes, even if user.name is the same. Mature state libraries usually add selector support so components subscribe to slices efficiently.
Selector concerns:
- Selected value equality.
- Stable selected references.
- Avoiding broad rerenders.
- Memoized derived data.
- Store update granularity.
This is one reason teams often use a library instead of writing a custom store by hand.
Server Rendering and getServerSnapshot
getServerSnapshot is used during server rendering and hydration.
const value = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot,
);
It should return the same initial value on the server and during client hydration.
Example:
function getServerSnapshot() {
return initialStoreStateFromHtml;
}
If the server snapshot and first client snapshot differ, hydration mismatches can happen. Browser-only state such as navigator.onLine often uses a conservative server fallback.
Browser API Example: Media Query
useSyncExternalStore can wrap browser APIs.
function subscribeToMediaQuery(query: string, callback: () => void) {
const mediaQuery = window.matchMedia(query);
mediaQuery.addEventListener("change", callback);
return () => mediaQuery.removeEventListener("change", callback);
}
function useMediaQuery(query: string) {
return useSyncExternalStore(
(callback) => subscribeToMediaQuery(query, callback),
() => window.matchMedia(query).matches,
() => false,
);
}
This hook exposes external browser state as a React-readable value.
Local Storage and Cross-Tab State
The browser storage event can notify other tabs when localStorage changes.
function subscribeToStorage(callback: () => void) {
window.addEventListener("storage", callback);
return () => window.removeEventListener("storage", callback);
}
function getThemeSnapshot() {
return localStorage.getItem("theme") ?? "light";
}
function useStoredTheme() {
return useSyncExternalStore(
subscribeToStorage,
getThemeSnapshot,
() => "light",
);
}
Note that the storage event fires in other documents, not usually in the same tab that made the change. A robust store wrapper should also notify same-tab subscribers when it writes.
Immutable Updates
External stores should notify subscribers only after a meaningful change and should expose stable snapshots.
Bad mutable pattern:
state.user.name = "New name";
emitChange();
If getSnapshot returns the same state reference, React may not detect a changed snapshot correctly.
Better:
state = {
...state,
user: {
...state.user,
name: "New name",
},
};
emitChange();
Immutable updates make snapshot identity meaningful.
Tearing
Tearing means different parts of the UI observe different versions of external state during the same render. useSyncExternalStore exists to help React coordinate external subscriptions with rendering so components read consistent snapshots.
Application developers do not usually need to implement complex tearing logic themselves, but they should understand the contract:
getSnapshotmust be pure.- Snapshots must be stable.
- Subscriptions must notify on changes.
- The store should not mutate state behind React's back without notification.
If the contract is broken, UI consistency breaks.
When to Use It Directly
Use useSyncExternalStore directly when building:
- A custom external store.
- A wrapper around a browser API.
- A library integration.
- A bridge to a non-React state source.
- A low-level hook used across an app.
Do not reach for it for ordinary component state. Use useState, useReducer, context, or a state library first.
Most application developers use useSyncExternalStore indirectly through libraries.
Common Mistakes
Common mistakes include:
- Returning a new object from
getSnapshotevery call. - Mutating store state without changing snapshot identity.
- Forgetting to unsubscribe.
- Creating
subscribeinside render without stability. - Using
useSyncExternalStorefor local component state. - Omitting
getServerSnapshotin SSR scenarios that need it. - Returning different server and hydration snapshots.
- Building a complex store without selector/equality support.
- Not notifying subscribers after store changes.
Best Practices
Best practices include:
- Keep
getSnapshotpure and stable. - Return the same snapshot reference when state has not changed.
- Notify subscribers after every meaningful store change.
- Return an unsubscribe function from
subscribe. - Use immutable snapshots or cache derived snapshots.
- Provide
getServerSnapshotfor server-rendered apps. - Wrap external stores in custom hooks.
- Prefer established libraries for complex app-wide stores.
- Add selector/equality support if many consumers need different slices.
- Test subscription, unsubscribe, unchanged snapshots, and SSR fallback behavior.