External Store Synchronization with useSyncExternalStore0%

External Store Synchronization with useSyncExternalStore

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

External Store Synchronization with useSyncExternalStore

Prior to React 18, state management libraries (Redux, Zustand, MobX) relied on useEffect and useState to subscribe to external mutable stores. With the arrival of Concurrent React and interruptible rendering, this naive approach suffered from tearing: a visual bug where different components in the same tree render conflicting data from the same store because the store mutated midway through an interrupted render. useSyncExternalStore solves tearing with guaranteed synchronous consistency.


1. What is "Tearing"?

In graphics and UI development, tearing occurs when a frame displays data from two different points in time:

Output
Concurrent Render Timeline:
1. Component A renders with Store State: { count: 1 }
2. React pauses render to handle high-priority user click
3. External store mutates to: { count: 2 }
4. React resumes render: Component B renders with Store State: { count: 2 }
RESULT: Component A displays "1", Component B displays "2" in the exact same view!

useSyncExternalStore guarantees that any external store subscription is read synchronously without tearing across the entire concurrent render.


2. API Signature

TSX
const state = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot // Optional: for SSR hydration
);
  • subscribe(callback): Registers a listener that is invoked whenever the store mutates. Must return a cleanup unsubscribe function.
  • getSnapshot(): Returns an immutable snapshot of current store data. If the store hasn't changed, it must return the exact same referential value.
  • getServerSnapshot(): Returns the snapshot during SSR HTML rendering.

3. Subscribing to Browser APIs: window.navigator.onLine

A common use case is subscribing to native browser events without third-party libraries:

TSX
import React, { useSyncExternalStore } from "react";
 
// 1. Subscribe function: attaches browser event listeners
function subscribeOnlineStatus(callback: () => void) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
 
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
 
// 2. Client snapshot getter
function getOnlineSnapshot() {
return navigator.onLine;
}
 
// 3. Server snapshot getter (default to true during SSR)
function getServerSnapshot() {
return true;
}
 
// Custom hook
export function useOnlineStatus() {
return useSyncExternalStore(
subscribeOnlineStatus,
getOnlineSnapshot,
getServerSnapshot
);
}
 
export function NetworkStatusBadge() {
const isOnline = useOnlineStatus();
 
return (
<div className="p-4 bg-slate-900 text-white rounded-lg flex items-center gap-3">
<div className={`h-3 w-3 rounded-full ${isOnline ? "bg-emerald-500" : "bg-rose-500 animate-ping"}`} />
<span className="text-sm font-semibold">
{isOnline ? "Connected to Cloud Services" : "Offline: Check Network Connection"}
</span>
</div>
);
}

4. Subscribing to Media Queries (window.matchMedia)

TSX
import { useSyncExternalStore } from "react";
 
export function useMediaQuery(query: string) {
const subscribe = (callback: () => void) => {
const matchMediaList = window.matchMedia(query);
matchMediaList.addEventListener("change", callback);
return () => matchMediaList.removeEventListener("change", callback);
};
 
const getSnapshot = () => window.matchMedia(query).matches;
const getServerSnapshot = () => false;
 
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
 
// Usage:
// const isMobile = useMediaQuery("(max-width: 768px)");

5. Critical Rule: Stable getSnapshot References

The function returned by getSnapshot must return a cached or primitive value. If getSnapshot returns a new object literal ({ count: store.count }) on every invocation, React detects an infinite change loop and crashes with:

"Maximum update depth exceeded."

Always return primitive types or referentially stable memoized slices from getSnapshot.


Interactive Quiz

Practice Quiz

Question 1
What is "tearing" in Concurrent React?
Question 2
Which hook was introduced in React 18 specifically to eliminate tearing in external state libraries?
Question 3
What must the subscribe parameter passed to useSyncExternalStore return?
Question 4
Why must getSnapshot() return referentially identical values if the store has not changed?
Question 5
What is the purpose of the third argument (getServerSnapshot) in useSyncExternalStore?
Next Lesson

Virtualizing Massive Lists with TanStack Virtual

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext