external state, safely synced
useState/useReducer own state inside React.
useSyncExternalStore is for the world outside React — a
custom store, window.innerWidth, navigator.onLine,
Zustand, Redux, a WebSocket. You hand it a subscribe function and a
getSnapshot function; it reads the snapshot, subscribes, and re-renders
the moment the store notifies. Crucially, during one concurrent render pass every
component reads the same snapshot — so the UI can
never tear (show two inconsistent views of one store).
| piece | role | analogy |
|---|---|---|
subscribe(cb) |
register a listener; return an unsubscribe function | hand the store your phone number — and a way to hang up |
getSnapshot() |
return the current snapshot — must be immutable & cached | the photo of state taken right now |
getServerSnapshot() |
initial snapshot for SSR / hydration (optional, 3rd arg) | the photo taken before the page is interactive |
| snapshot stability | same data → same reference (Object.is) |
break this → infinite re-render loop |
1 · the store you write (edit me)
2 · Babel compiles JSX → element tree
<App/> becomes React.createElement(App).
Each store.setState(...) fires every listener → React reads
store.getState() again → re-renders all subscribers with one
consistent snapshot.
// (hit "compile & render" to see Babel's output)
3 · live React (the subscribe/getSnapshot contract, proven)
Click "+1" — it calls store.setState(), which notifies the listeners
React registered. StoreCounter and Mirror are independent
components reading the same store; both update in lockstep.
The gold-check does this automatically: assert
Count: 0, click +1 three times, assert 1 → 2 → 3, then confirm
the mirror is in sync.
counter: — · mirror: — · gold: 0 → click+1×3 → 1,2,3 + mirror in sync
intent → pattern
| intent | pattern | why |
|---|---|---|
| subscribe to a custom store | useSyncExternalStore(store.subscribe, store.getState) |
React registers a listener; re-renders when the store notifies |
| subscribe to a browser API | useSyncExternalStore(subResize, () => window.innerWidth) |
reactive innerWidth/onLine/media-query without manual useEffect |
| cache the snapshot | store a stable ref; return it unchanged when data is the same | if getSnapshot returns a new ref each call → infinite loop |
| server-side rendering | pass getServerSnapshot as the 3rd argument |
consistent first paint before hydration; avoids hydration mismatch |
| mutate the store | store.setState(s => ({...s, count: s.count+1})) + notify |
every subscriber reads the new immutable snapshot in one render pass |