React useSyncExternalStore in 2026: The Hook Every State Library Uses and Why You Should Understand It
This article was written with the assistance of AI, under human supervision and review.
Most React state management confusion stems from teams treating external stores as if they're native React state. The assumption breaks in React 18's concurrent rendering model, where a component can read from a store twice during a single render and receive different values. This inconsistency—called tearing—corrupts UI state in ways that are expensive to debug and embarrassing to ship.
The pattern that eliminates tearing is useSyncExternalStore, the hook that every production state library now uses under the hood. Developers dismiss it as "library internals" without realizing it's the foundation that makes Zustand, Jotai, and Redux work correctly in concurrent mode. When you understand this hook, you understand why your state library behaves the way it does—and when you need to build a custom store, you know exactly how to integrate it safely.
React 18 introduced time-slicing and transitions that allow renders to pause and resume. A store outside React's control can change during that pause. Without useSyncExternalStore, the resumed render sees new data while sibling components still reference old snapshots. The UI enters an inconsistent state where a product list shows five items but the cart count displays four.
useSyncExternalStore solves this by forcing React to commit any in-progress render when the external store changes. The guarantee is simple: every component in a render tree sees the same snapshot value, even when renders interleave or suspend. This synchronization is what "sync" means in the hook's name—it's not about synchronous code execution, it's about snapshot consistency.
Key Takeaways useSyncExternalStore prevents tearing by ensuring all components in a concurrent render read the same snapshot value from an external store. The hook requires three pieces: a stable subscribe function that registers a listener, a getSnapshot function that returns immutable data, and optionally a getServerSnapshot for SSR hydration. Every major state library (Zustand, Jotai, Redux Toolkit) now uses useSyncExternalStore internally—understanding it reveals why their APIs enforce certain patterns like immutable updates. Building a custom store with this hook takes fewer than 50 lines but demands strict adherence to immutability and subscription stability to avoid infinite loops. Choose useSyncExternalStore for any data source outside React's control (browser APIs, WebSockets, shared workers); use Context for component-tree-scoped state that doesn't need external sync.
useSyncExternalStore accepts three arguments, and their interaction determines whether the integration succeeds or fails. The first argument is subscribe, a function that takes a callback and registers it with the external store. When the store changes, it must invoke all registered callbacks. The second argument is getSnapshot, which returns the current store value. React calls this function during render and compares the returned reference to detect changes. The optional third argument is getServerSnapshot, which provides the initial value during server-side rendering when the external store doesn't exist yet.
The contract between these functions is rigid. The subscribe function must return an unsubscribe function that removes the callback when the component unmounts. React expects this cleanup to prevent memory leaks when components re-render or unmount. The failure mode here is subtle: if subscribe returns undefined or a non-function, React silently skips cleanup and the callback continues firing after the component is gone, updating state that no longer exists.
The getSnapshot function must return the same reference for equal values. React uses Object.is comparison to determine if a re-render is necessary. Returning a new object on every call—even if the contents are identical—triggers infinite render loops. This is why store implementations typically cache snapshots and only create new references when the underlying data actually changes. The discipline required here is stricter than useMemo or useCallback because React cannot fix violations for you.
The getServerSnapshot argument addresses the timing mismatch between server and client. On the server, external stores like localStorage or WebSocket connections don't exist. React needs a value to render the initial HTML. When the client hydrates, it must use the same initial value to match the server-rendered markup, then switch to the live store. Omitting getServerSnapshot when targeting SSR causes hydration mismatches that manifest as content flashes or suppressed event handlers.
The clearest way to understand useSyncExternalStore is to build a custom hook that wraps browser storage. The requirement is simple: when localStorage changes in one tab, all subscribed components across all tabs must re-render with the new value. This cross-tab synchronization is exactly the kind of external data source that Context cannot handle and where custom event listeners fail without careful subscription management.
The implementation maintains a listeners Set to track subscribed components. When setState is called, it updates both the in-memory currentValue and localStorage, then notifies all listeners. This dual update is critical: updating only localStorage would miss components in the same tab, while updating only memory would miss cross-tab synchronization.
The storage event listener handles changes from other tabs. Browser storage events only fire in tabs that did not trigger the change. When another tab calls setTheme, the event fires in this tab, updating currentValue and notifying local listeners. Without this listener, the hook would only work within a single tab—a common mistake when developers test in one browser window.
The useMemo call ensures createStorageStore runs once per unique key. Without it, every render would create a new store instance with a new subscribe function, and React would treat that as a subscription change, triggering unsubscribe-then-resubscribe on every render. This creates a memory leak where old listeners accumulate because the cleanup function references a stale listeners Set.
State libraries adopted useSyncExternalStore to eliminate tearing without forcing developers to understand the hook directly. The abstraction works because these libraries control the store implementation and can guarantee the subscription and snapshot contracts. When developers call useStore or useAtom, they're indirectly invoking useSyncExternalStore with library-managed functions.
Zustand exposes a subscribe method on the store object that accepts a listener function. Internally, it maintains a Set of listeners identical to the storage example above. When developers call set to update state, Zustand updates the internal store object, then invokes every listener. The useStore hook wraps this with useSyncExternalStore, passing the store's subscribe method directly and a getSnapshot function that returns the current state reference.
Jotai takes a different approach because its atoms are independent units rather than a single store. Each atom has its own subscription mechanism, but the library maintains a global WeakMap that tracks which atoms are mounted and which components subscribe to each. When an atom's value changes, Jotai looks up all subscribed components in the WeakMap and notifies them. The useAtom hook calls useSyncExternalStore with an atom-specific subscribe function that registers the component in this WeakMap.