useSyncExternalStore
Reviewed & published by Brayan K
useSyncExternalStore is a React Hook for subscribing to data that lives outside React — a browser API, a global object, or a state library — in a way that stays consistent during concurrent rendering and avoids "tearing." You give it a subscribe function and a getSnapshot function, and React keeps your component in sync.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- What an 'external store' is and why React needs help reading it
- Writing subscribe(callback) that returns an unsubscribe fn
- Writing getSnapshot() that returns the current value
- Adding getServerSnapshot() for SSR-safe hydration
- What 'tearing' is and how this hook prevents it
- Why state libraries use it under the hood
1️⃣ The Two Required Functions
The hook needs a subscribe(callback) that registers a listener and returns a cleanup/unsubscribe function, plus a getSnapshot() that synchronously returns the current value. When the store changes it calls the listeners; React then calls getSnapshot and re-renders if the value differs.
import { useSyncExternalStore } from "react";
function useOnlineStatus() {
return useSyncExternalStore(
(cb) => { // subscribe
window.addEventListener("online", cb);
window.addEventListener("offline", cb);
return () => { // unsubscribe
window.removeEventListener("online", cb);
window.removeEventListener("offline", cb);
};
},
() => navigator.onLine, // getSnapshot
() => true // getServerSnapshot
);
}// An external store is anything React doesn't own: a browser API,
// a global object, a tiny pub/sub. It needs two things:
// - subscribe(listener): start listening, return an unsubscribe fn
// - getSnapshot(): return the current value RIGHT NOW
let isOnline = true;
const listeners = new Set();
function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener); // unsubscribe
}
function getSnapshot() {
return isOnline; // current value
}
function setOnline(next) { // pretend the network changed
isOnline = next;
listeners.forEach((l) => l()); // notify React
}
const stop = subscribe(() => console.log("changed ->", getSnapshot()));
setOnline(false); // changed -> false
setOnline(true); // changed -> true
stop(); // unsubscribe so we stop hearing updates2️⃣ Tearing — Why This Hook Exists
In concurrent rendering React can pause mid-render. If two components read external state with plain useEffect, one might see the old value and one the new — the screen is "torn." Because every component reads through the same getSnapshot, this hook guarantees a single, consistent value for the whole render.
// 🚫 Risky: store value could change between these two reads
function Header() { const w = useStoreWithEffect(); /* ... */ }
function Sidebar() { const w = useStoreWithEffect(); /* could differ! */ }
// ✅ Safe: useSyncExternalStore gives both the SAME snapshot
function Header() { const w = useSyncExternalStore(sub, getSnap); }
function Sidebar() { const w = useSyncExternalStore(sub, getSnap); }// TEARING: two reads of the same store during one render disagree.
// useState + useEffect can show a stale value in one place and a
// fresh value in another. A single getSnapshot avoids that by
// giving everyone the SAME value for the whole render.
let width = 1024;
function getSnapshot() { return width; }
// Both components read through the SAME getSnapshot in one render:
const headerWidth = getSnapshot();
const sidebarWidth = getSnapshot();
console.log("header:", headerWidth); // 1024
console.log("sidebar:", sidebarWidth); // 1024 (consistent!)
console.log("torn?", headerWidth !== sidebarWidth); // false3️⃣ Build Your Own Subscribe + Snapshot
The pattern is small once you've seen it: keep a set of listeners, subscribe adds one and returns a remover, getSnapshot returns the current value, and your setters notify all listeners. One caution: getSnapshot must return a stable reference — return primitives or a cached object, never a freshly built one each call, or you'll loop.
const store = {
value: 0,
listeners: new Set(),
subscribe(cb) { store.listeners.add(cb); return () => store.listeners.delete(cb); },
getSnapshot() { return store.value; }, // primitive -> stable
set(next) { store.value = next; store.listeners.forEach((l) => l()); },
};
// In a component: const v = useSyncExternalStore(store.subscribe, store.getSnapshot);// 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
// Build the two functions useSyncExternalStore needs.
// 1) subscribe must RETURN the unsubscribe function.
// 2) getSnapshot must RETURN the current value.
let value = "light";
const listeners = new Set();
function subscribe(listener) {
listeners.add(listener);
return ___; // 👉 () => listeners.delete(listener)
}
function getSnapshot() {
return ___; // 👉 value
}
const stop = subscribe(() => console.log("theme:", getSnapshot()));
value = "dark"; listeners.forEach((l) => l()); // theme: dark
console.log("snapshot now:", getSnapshot()); // snapshot now: dark
stop();
// ✅ Expected output:
// theme: dark
// snapshot now: dark🧩 Reorder Challenge
These lines build an online-status hook. Put them in the right order:
A. window.addEventListener("online", cb);
B. return useSyncExternalStore(
C. () => navigator.onLine,
D. (cb) => {
E. return () => window.removeEventListener("online", cb);Common Errors (and the fix)
- Infinite re-render loop. — getSnapshot returns a new object every call. Return a primitive or cache the reference so it only changes when data changes.
- subscribe doesn't return a cleanup. — React can't unsubscribe and you leak listeners. Always return () => removeListener().
- Hydration mismatch in SSR. — Provide getServerSnapshot returning a server-safe default, since browser APIs like navigator don't exist on the server.
- Reaching for it for ordinary app state. — Prefer useState/context or a library. This hook is for genuinely external sources.
📋 Quick Reference
| Argument | Role |
|---|---|
| subscribe(cb) | Listen; return an unsubscribe fn |
| getSnapshot() | Return the current value (stable ref) |
| getServerSnapshot() | Value used during SSR/hydration |
| returns | The current snapshot, re-rendered on change |
🧠 Quick Recall
1. What must subscribe return?
An unsubscribe (cleanup) function, so React can stop listening when the component unmounts.
2. Why must getSnapshot return a stable reference?
React compares snapshots to decide whether to re-render. A new object every call looks like a change every time and causes an infinite loop.
3. What problem is "tearing"?
Different parts of the UI reading different values of the same store in one render. A single shared getSnapshot prevents it.
Mini-Challenge: A Tiny Counter Store
Build the smallest possible external store — getSnapshot, subscribe, and an increment that notifies listeners. Run it and check your output.
// 🎯 MINI-CHALLENGE: a tiny counter store
//
// Build a store with:
// - getSnapshot() returning the current count
// - subscribe(l) that returns an unsubscribe fn
// - increment() that bumps count and notifies listeners
// Then subscribe, increment twice, and log each snapshot.
//
// ✅ Expected output:
// count -> 1
// count -> 2
let count = 0;
const listeners = new Set();
// your code here🎉 Lesson Complete
- ✅ useSyncExternalStore safely subscribes to state outside React
- ✅ Provide subscribe (returns unsubscribe) and getSnapshot
- ✅ Add getServerSnapshot for SSR-safe hydration
- ✅ It prevents tearing; keep getSnapshot's return stable
Practice quiz
What is useSyncExternalStore designed for?
- Managing ordinary component state
- Replacing useMemo for expensive calculations
- Subscribing to data that lives outside React in a tearing-safe way
- Fetching data from a REST API
Answer: Subscribing to data that lives outside React in a tearing-safe way. useSyncExternalStore subscribes to an external store (browser API, global object, state library) and keeps reads consistent during concurrent rendering.
What are the two required arguments to useSyncExternalStore?
- subscribe and getSnapshot
- initialValue and reducer
- ref and callback
- deps and effect
Answer: subscribe and getSnapshot. You pass subscribe(callback) and getSnapshot(). A third optional getServerSnapshot is used for SSR.
What must the subscribe function return?
- The current value of the store
- A boolean indicating success
- A promise that resolves when subscribed
- An unsubscribe (cleanup) function
Answer: An unsubscribe (cleanup) function. subscribe registers a listener and must return a function that removes it, so React can unsubscribe when the component unmounts.
What does getSnapshot do?
- Asynchronously fetches the next value
- Synchronously returns the store's current value
- Schedules a re-render after a delay
- Serializes the store to JSON
Answer: Synchronously returns the store's current value. getSnapshot returns the current value synchronously. React calls it to read the store and compares results to decide whether to re-render.
Why must getSnapshot return a stable reference?
- React compares snapshots; a new object every call looks like a change and causes an infinite loop
- To keep the store small in memory
- Because getSnapshot must be async
- So it can be cached on the server
Answer: React compares snapshots; a new object every call looks like a change and causes an infinite loop. React re-renders when the snapshot differs. Returning a freshly built object each call always looks 'changed', triggering an infinite re-render loop. Return primitives or a cached reference.
What is 'tearing'?
- When a component throws during render
- When a network request fails
- When different parts of the UI read different values of the same store in one render
- When state updates are batched together
Answer: When different parts of the UI read different values of the same store in one render. Tearing is inconsistency where one part of the screen shows the old value and another the new. A shared getSnapshot gives everyone the same value per render.
What is the optional third argument getServerSnapshot for?
- Caching network responses
- Providing the value used during server rendering so hydration matches
- Throttling store updates
- Logging store changes to the console
Answer: Providing the value used during server rendering so hydration matches. getServerSnapshot supplies an SSR-safe value (browser APIs like navigator don't exist on the server), keeping client hydration consistent.
Which is a good use case for writing useSyncExternalStore directly?
- Storing a form's input value
- Memoizing a derived list
- Passing props down a few levels
- Subscribing to browser online/offline status or window size
Answer: Subscribing to browser online/offline status or window size. It shines for genuinely external sources like navigator.onLine, window size, or media queries. Ordinary app state should use useState/context or a library.
How do popular state libraries like Redux and Zustand relate to this hook?
- They forbid its use
- They call useSyncExternalStore internally to subscribe React components safely
- It replaces them entirely for all app state
- They only work in class components instead
Answer: They call useSyncExternalStore internally to subscribe React components safely. Modern store libraries call useSyncExternalStore under the hood, which is why their hooks integrate cleanly with concurrent React.
If subscribe fails to return a cleanup function, what happens?
- Nothing — it is optional
- The component refuses to mount
- React can't unsubscribe and you leak listeners
- getSnapshot stops being called
Answer: React can't unsubscribe and you leak listeners. Without a returned cleanup, React cannot remove the listener on unmount, leaking subscriptions. Always return () => removeListener().
Continue this course
- Previous: useDeferredValue
- Next: forwardRef & useImperativeHandle — Pass refs to children and expose an imperative API like focus() or scrollTo()
- Quick reference: React cheat sheet
Frequently asked questions
Why not just use useState + useEffect to read external state?
That approach can 'tear' — different parts of the screen read different values during a single render, especially with concurrent rendering. useSyncExternalStore is designed to give every component the same, consistent snapshot, so it's the safe way to subscribe to anything outside React.
What are the three arguments?
subscribe(callback) registers a listener and returns an unsubscribe function; getSnapshot() returns the current value synchronously; and the optional getServerSnapshot() returns the value used during server rendering so hydration matches. React calls them to stay in sync with the store.
Why must getSnapshot return a stable reference?
React compares the snapshot to the previous one to decide whether to re-render. If getSnapshot builds a new object/array every call, React thinks it changed every time and re-renders in a loop. Return primitives or a cached reference that only changes when the data actually changes.
When would I write this myself versus use a library?
Most app state should use a library (Redux, Zustand) which already calls useSyncExternalStore internally. You write it directly to subscribe to browser APIs — online status, window size, media queries — or a tiny custom store.