Suggest an editImprove this articleRefine the answer for “What is useSyncExternalStore in React?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)`useSyncExternalStore` is a React 18 hook for subscribing to external data sources (browser APIs, third-party stores) while preventing tearing during concurrent rendering. React captures one atomic snapshot per render pass and reuses it for the entire tree. ```tsx const isOnline = useSyncExternalStore( (cb) => (window.addEventListener("online", cb), () => window.removeEventListener("online", cb)), () => navigator.onLine, () => true // SSR default ); ``` **Key point:** use it for any data that lives outside React's state system.Shown above the full answer for quick recall.Answer (EN)Image**`useSyncExternalStore`** is a React 18 hook that lets components subscribe to external data sources while guaranteeing the same snapshot value across all reads in a single render pass, preventing tearing during concurrent rendering. ## Theory ### TL;DR - Think of it as a **shared whiteboard**: every component in a render sees the exact same value, never a mix of old and new data - The problem it solves: concurrent React can pause and resume mid-render, so a plain `navigator.onLine` read might return different values at different points in the same render - Three arguments: `subscribe` (register a listener), `getSnapshot` (read the current value), `getServerSnapshot` (optional, for SSR) - Use for browser APIs, third-party stores, and globals outside React. Skip for `useState`/`useReducer` - React already handles those without tearing ### Quick example ```tsx function useOnlineStatus() { return useSyncExternalStore( // subscribe: attach listener, return cleanup (cb) => { window.addEventListener("online", cb); window.addEventListener("offline", cb); return () => { window.removeEventListener("online", cb); window.removeEventListener("offline", cb); }; }, () => navigator.onLine, // getSnapshot: pure read () => true // getServerSnapshot: SSR default ); } ``` React calls `getSnapshot` once per render and reuses that value for the entire tree. No mutation happening mid-render can change what a component sees. ### What tearing actually is Concurrent React splits work into units of varying priority. While processing a large component tree, the scheduler can pause, let higher-priority work run, then resume. If code reads `navigator.onLine` directly, one part of the tree might read `true` (before the network change) and another reads `false` (after the change) in the same commit. The UI shows both states at once. That is tearing. `useSyncExternalStore` fixes this by locking in one atomic value from `getSnapshot` for the entire render pass. ### When to use it - Browser API changes (online/offline status, media queries, `window.innerWidth`) - use `useSyncExternalStore` - Third-party stores without built-in React bindings (Zustand internals, Redux DevTools) - use `useSyncExternalStore` - Any mutable value that lives outside React and can change without React knowing - use `useSyncExternalStore` - Plain React state (`useState`, `useReducer`) - skip this hook, React already handles those without tearing ### How it works internally React calls `getSnapshot()` once per render pass and caches the result per component. All children reading from the same source get that cached value. When the `subscribe` callback fires, React schedules a re-render via its scheduler. The callback reference stays stable across renders, so subscriptions do not accumulate. On the server, `getServerSnapshot` runs instead of `getSnapshot` because `window` is not available there. One thing I noticed in production apps: the most common footgun is returning a new object inside `getSnapshot`. React compares snapshots by reference, so returning `{ count: store.count }` on every call triggers infinite re-renders. Return primitives or stable references. ### Common mistakes **Returning a new object in `getSnapshot`** ```tsx // Wrong: new object on every call = infinite re-renders const getSnapshot = () => ({ count: externalStore.count }); // Right: return a primitive const getSnapshot = () => externalStore.count; ``` React calls `getSnapshot` multiple times and checks if the result changed. Different reference equals re-render. Always return primitives or memoize object snapshots. **Mutating state inside `getSnapshot`** ```tsx // Wrong: side effect inside snapshot read const snap = () => { externalStore.value++; return externalStore.value; }; // Right: pure read only const snap = () => externalStore.value; ``` `getSnapshot` must be a pure function. Side effects here cause infinite update loops. **Missing cleanup in `subscribe`** ```tsx // Wrong: listener never removed (cb) => { window.addEventListener("resize", cb); } // Right: return the cleanup function (cb) => { window.addEventListener("resize", cb); return () => window.removeEventListener("resize", cb); } ``` Without cleanup, each re-render stacks another listener. Memory leak, plus callbacks fire multiple times per event. **Skipping `getServerSnapshot`** ```tsx // Wrong: crashes on server where window is undefined useSyncExternalStore(subscribe, () => window.innerWidth); // Right: provide a server-safe default useSyncExternalStore(subscribe, () => window.innerWidth, () => 1024); ``` Omitting the third argument causes hydration mismatch errors. The server renders with no value, the client hydrates with a real one, React warns or throws. **Using it for plain React state** If your data lives in `useState` or `useReducer`, this hook adds nothing. React already manages those without tearing. `useSyncExternalStore` is only for data that lives outside React's control. ### Real-world usage - Zustand (v4+): uses `useSyncExternalStore` internally for all store subscriptions - Redux Toolkit: powers DevTools time-travel snapshots - TanStack Query: tracks `window` focus and blur for background refetch logic - Framer Motion: reads pointer and gesture APIs from the browser - Any production responsive hook (media queries, viewport dimensions) that needs to avoid layout shifts ### Follow-up questions **Q:** What is tearing and why does it only matter in concurrent React? **A:** Tearing is when two parts of the same render read an external value at different moments and get different results, producing an inconsistent UI. In synchronous React, renders are uninterruptible, so this cannot happen. Concurrent React pauses and resumes work, which makes it possible. **Q:** How is this different from `useEffect` + `useState`? **A:** The `useEffect` approach lags by one render. You subscribe in an effect, set state, then re-render. With `useSyncExternalStore`, the read is synchronous and tied to the render itself. No stale render, no intermediate flash. **Q:** What happens on SSR if you skip `getServerSnapshot`? **A:** React throws in development and produces a hydration mismatch in production. Always pass a server-safe default that does not rely on browser APIs. **Q:** Why must `getSnapshot` return a consistent value between calls if nothing changed? **A:** React calls `getSnapshot` multiple times during concurrent work to check for mid-render mutations. If it returns different values on repeated calls without the store actually changing, React logs a warning and may force a synchronous re-render. **Q (senior):** How does `useSyncExternalStore` interact with `startTransition`? **A:** `startTransition` marks state updates as low priority, deferring them. External store updates wrapped with `useSyncExternalStore` are treated as synchronous by React's scheduler. If a store change fires during a transition, React flushes it synchronously to prevent tearing, even if that interrupts the ongoing transition work. ## Examples ### Tracking online status ```tsx function useOnlineStatus() { return useSyncExternalStore( (cb) => { window.addEventListener("online", cb); window.addEventListener("offline", cb); return () => { window.removeEventListener("online", cb); window.removeEventListener("offline", cb); }; }, () => navigator.onLine, () => true // server: assume online ); } function NetworkBanner() { const isOnline = useOnlineStatus(); if (isOnline) return null; return <div className="banner">You are offline</div>; } ``` `getSnapshot` is a pure read of `navigator.onLine`. The same value is returned for the entire render pass, regardless of when the network event actually fires. ### Responsive layout with media query ```tsx function useIsMobile() { return useSyncExternalStore( (cb) => { const mql = window.matchMedia("(max-width: 768px)"); mql.addEventListener("change", cb); return () => mql.removeEventListener("change", cb); }, () => window.matchMedia("(max-width: 768px)").matches, () => false // SSR: assume desktop ); } function Sidebar() { const isMobile = useIsMobile(); return ( <nav className={isMobile ? "sidebar-mobile" : "sidebar-desktop"}> {/* consistent layout, no flash on resize */} </nav> ); } ``` The SSR default of `false` avoids hydration mismatches on desktop-first layouts. Without `getServerSnapshot`, the server and client render different class names and React throws a warning. ### External store integration (how Zustand works under the hood) ```tsx const counterStore = { count: 0, listeners: new Set<() => void>(), increment() { this.count++; this.listeners.forEach((l) => l()); }, subscribe(cb: () => void) { this.listeners.add(cb); return () => this.listeners.delete(cb); }, getSnapshot() { return this.count; }, }; function Counter() { const count = useSyncExternalStore( counterStore.subscribe.bind(counterStore), counterStore.getSnapshot.bind(counterStore), () => 0 ); return ( <div> <p>Count: {count}</p> <button onClick={() => counterStore.increment()}>+1</button> </div> ); } ``` The store mutates directly outside React, but `useSyncExternalStore` bridges it into the render cycle. Each `increment` call notifies all listeners, React schedules a re-render, and `getSnapshot` returns the new count. This is roughly how Zustand v4 implements its React bindings.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.