useSyncExternalStore: The Right Way to Subscribe to External State
Before useSyncExternalStore, subscribing to an external store in a React component caused tearing — different parts of the tree reading different versions of the store in the same render. This hook exists to fix that, and it is the right foundation for any custom store.
Most React state lives inside components — useState, useReducer, context. But some state lives outside React entirely: Redux stores, browser APIs like localStorage, or custom data sources that multiple parts of an app need to share.
Before React 18, subscribing to external state in concurrent mode caused tearing — different components reading different versions of the same external store during a single render pass. React would start rendering, the store would update mid-render, and different components would see inconsistent data. The UI would display values from two different points in time simultaneously.
useSyncExternalStore was introduced in React 18 to fix this. It guarantees that every component reading from the same external store sees the same snapshot during a single render.
The API
const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);Three arguments:
subscribe(listener) — registers a callback that React will call whenever the store changes. Must return a cleanup function that unregisters the listener. React uses this to know when to re-read the store.
getSnapshot() — returns the current value of the store synchronously. React calls this on every render. Must return the same value if the store has not changed — React uses referential equality to decide whether to re-render.
getServerSnapshot() — optional. Returns the store's value during server-side rendering and hydration. Required if you use the hook in a component that renders on the server.
Building a Custom External Store
The pattern becomes clear with a concrete example. A Pokemon data store that lives outside any component:
// pokemonStore.js
let state = { pokemons: [] };
let listeners = [];
export async function fetchPokemons() {
const res = await fetch('https://pokeapi.co/api/v2/pokemon');
const data = await res.json();
state = { pokemons: data.results };
listeners.forEach(listener => listener()); // notify all subscribers
}
export function getState() {
return state;
}
export function subscribe(listener) {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
}The store holds its own state, manages its own listeners, and notifies them when data changes. It has no React dependency — it is a plain JavaScript module.
Subscribing from a Component
import { useSyncExternalStore, useEffect } from 'react';
import { subscribe, getState, fetchPokemons } from './pokemonStore';
function PokemonList() {
const { pokemons } = useSyncExternalStore(subscribe, getState);
useEffect(() => {
fetchPokemons();
}, []);
return (
<ul>
{pokemons.map(p => <li key={p.name}>{p.name}</li>)}
</ul>
);
}The component subscribes to the store with two lines. When fetchPokemons updates the state and calls every listener, React re-reads getState and re-renders — guaranteed to be consistent across the entire component tree.
Any other component can subscribe to the same store with the same two lines. No duplication of fetch logic, no prop threading, no context wrapper needed.
useSyncExternalStore vs useContext
Both can share data across components, but they serve different purposes.
useContext shares state that lives inside React — values computed in a component and made available to descendants. It works well for infrequently changing data like theme, locale, or auth status. The limitation: every consumer re-renders whenever the context value changes, even if the slice it uses did not change.
useSyncExternalStore is for state that lives outside React — third-party stores, browser APIs, custom data modules. It is built for concurrent rendering and avoids tearing. Components only re-render when the specific snapshot they depend on actually changes.
The practical guide: use context for global UI configuration (theme, auth, locale). Use useSyncExternalStore when syncing with external data sources, global state libraries, or any store that updates independently of React's rendering cycle.
Practice what you just read.
Keep reading