- React
- Advanced Hooks, Referential Equality, and Stores
- useSyncExternalStore: Tearing Prevention
useSyncExternalStore: Tearing Prevention
useSyncExternalStore safely subscribes components to external non-React state stores (browser APIs, Redux, Zustand) without concurrent tearing. It forces synchronous store reads during render and requires subscribe, getSnapshot, and getServerSnapshot callbacks.
Key Learning Objectives
Explain the "tearing" visual defect in Concurrent React when reading mutable external stores.
Implement custom browser subscriptions (window.navigator.onLine, matchMedia) using useSyncExternalStore.
Understand the required contracts: subscribe callback, getSnapshot immutable return, and getServerSnapshot.
Explain why library authors (Redux, Zustand) migrated from useEffect subscriptions to useSyncExternalStore.
The Interview Problem & Context
What is "tearing" in Concurrent React? Implement a custom hook useOnlineStatus() using useSyncExternalStore that subscribes to browser network status without tearing.
1import { useSyncExternalStore } from 'react';23// 1. Subscribe function: attaches listeners and returns cleanup4function subscribe(callback: () => void) {5 window.addEventListener('online', callback);6 window.addEventListener('offline', callback);7 return () => {8 window.removeEventListener('online', callback);9 window.removeEventListener('offline', callback);10 };11}1213// 2. Snapshot function: returns current immutable value14function getSnapshot() {15 return navigator.onLine;16}1718// 3. Server snapshot function: for SSR hydration safety19function getServerSnapshot() {20 return true; // Default to online during server render21}2223export function useOnlineStatus() {24 return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);25}
Deep Architectural Explanation
The Concurrent Tearing Problem
In synchronous React (React 17), rendering was uninterruptible.
In Concurrent React (React 18+), rendering can be paused and resumed across multiple time slices.
What is Tearing?
Imagine an external store (like Redux or a global variable) with color = "blue":
- Component A renders with
color = "blue". - React yields the thread to the browser.
- A user action or WebSocket event modifies the external store:
color = "red"! - React resumes rendering Component B with
color = "red".
Visual Tearing: Component A is blue and Component B is red on the exact same screen for the same store data! The UI is torn and inconsistent.
How useSyncExternalStore Fixes Tearing
useSyncExternalStore tells React:
- This state lives outside React's Fiber architecture.
- Whenever this store changes, React checks the snapshot. If the store changed mid-render, React discards the in-progress concurrent render and restarts the render pass synchronously, guaranteeing UI consistency!
The getSnapshot Invariant
getSnapshot must return a cached or immutable value. If you return a new object:
// ❌ INFINITE LOOP: Returns a new object reference on every call!
function getSnapshot() { return { online: navigator.onLine }; }React will detect a "changed" snapshot on every check, triggering an infinite re-render loop!
Common Interview Traps & Pitfalls
Returning a brand new object or array literal from getSnapshot(), causing infinite re-render loops.
Omitting the third argument (getServerSnapshot) in server-rendered applications.
Using useEffect to subscribe to external stores in React 18 instead of useSyncExternalStore.
Senior Interviewer Follow-Up Probes
Why did Zustand and Redux rewrite their core subscription mechanisms around useSyncExternalStore for React 18?
What happens if getSnapshot returns different values on consecutive calls without an external event firing?
How does useSyncExternalStore handle selector equality functions?
Frequently Asked Interview Questions
Why can't useEffect be used to subscribe to external stores in React 18?
Because useEffect runs asynchronously after the paint phase. Between the render phase and the effect execution, the external store could mutate, causing visual tearing.
Why must getSnapshot return a cached value?
React compares getSnapshot() returns using Object.is. Returning a new object reference triggers React to assume the store changed, causing an infinite re-render loop.
What is the role of getServerSnapshot?
It provides a deterministic snapshot during server-side rendering (where window or navigator APIs do not exist), preventing hydration mismatch errors.
Does useSyncExternalStore work with browser APIs like matchMedia?
Yes. It is the recommended modern React API for subscribing to window resize, matchMedia, geolocation, or online status.
