| |

React 53 ⚛️ Subscribing to External Stores with useSyncExternalStore

⏱️ Estimated read: 22 min | |

React is designed around a simple rule: components read data from props, state, and context. But real applications rarely stay inside that boundary. A WebSocket connection carries live messages. A browser API exposes the user’s online status. A third-party state library manages its own store outside React. When a component needs to read a value that lives and changes outside React, the traditional approach of combining useEffect and useState introduces a subtle but serious flaw: tearing. The component can render with one version of the store while another component on the same screen renders with a different version, producing an inconsistent UI. The useSyncExternalStore hook exists to solve exactly this problem, providing a first-class way to read and subscribe to external data sources in a way that is safe under concurrent rendering and server hydration .

Key point: useSyncExternalStore is not a replacement for useState or useReducer. It is the correct tool for integrating with data that lives outside React, whether that is a browser API, a third-party store, or any mutable source with its own subscription mechanism.


Why useSyncExternalStore exists

The tearing problem. When React renders concurrently, it can pause a render, work on something else, and resume later. During that pause, an external store might change. If a component reads from the store during render without a synchronization mechanism, it might see the old value on one render pass and the new value on another, or different components in the same tree might read different versions of the store. This inconsistency is called tearing, and it produces UIs that display contradictory information. The traditional useEffect + useState pattern is prone to this because the subscription and the read are not synchronized with React’s rendering lifecycle .

The subscription lifecycle problem. With useEffect, the subscription is set up after the component mounts and torn down when it unmounts. But React can mount, unmount, and remount components during concurrent rendering, and the subscription may not be correctly synchronized with the reads that happen during render. useSyncExternalStore integrates the subscription directly into React’s rendering model, so the store is read synchronously during render and the subscription is managed by React itself .

The server rendering problem. When a component that reads from a browser API is server-rendered, the API does not exist on the server. Without a mechanism to provide an initial snapshot, the server-rendered HTML and the client’s first render will mismatch, breaking hydration. useSyncExternalStore accepts an optional getServerSnapshot function that provides the value to use during server rendering and the initial client hydration .

The third-party integration problem. Before useSyncExternalStore, every state management library had to implement its own subscription mechanism, and each implementation had to handle the complexities of concurrent rendering, tearing, and hydration on its own. This led to duplicated effort and subtle bugs in libraries that did not get the details exactly right. By providing a built-in hook, React standardized the integration contract, shifting the responsibility for correctness into React itself .

The browser API problem. Many browser APIs expose mutable values that change over time: navigator.onLine, window.innerWidth, window.matchMedia results, localStorage. These values are not part of React’s state, and they can change without React’s knowledge. Reading them directly during render produces stale or inconsistent results. useSyncExternalStore provides a consistent way to read these values and re-render when they change .

The Transition consistency problem. When a store mutation happens during a non-blocking Transition update, React needs to ensure that all components on screen reflect the same version of the store. If the store changes during a Transition, React falls back to a blocking update, re-checking the snapshot just before applying changes to the DOM to guarantee consistency across the entire tree .


a. How useSyncExternalStore works

The hook accepts three arguments: a subscribe function, a getSnapshot function, and an optional getServerSnapshot function. It returns the current snapshot of the store, which the component uses in its render logic .

The subscribe function

The subscribe function takes a single callback argument and subscribes it to the store. When the store changes, it must invoke that callback, which signals React to re-read the snapshot and potentially re-render. The function must return a cleanup function that unsubscribes from the store. React calls the cleanup when the component unmounts or when the subscribe function itself changes between renders .

function subscribe(callback) {
  // Subscribe to the store.
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);

  // Return the cleanup function.
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

If the subscribe function is defined inside the component body without memoization, it is a new function on every render. React will unsubscribe from the store using the old function and resubscribe with the new one, which is inefficient and can cause bugs. The recommended practice is to declare subscribe outside the component, or wrap it in useCallback if it depends on props or state .

The getSnapshot function

The getSnapshot function returns the current value from the store. It is called during render to read the store’s state, and React may call it multiple times during a render pass to verify consistency. The function must be pure, fast, and synchronous. Critically, if the underlying store has not changed, getSnapshot must return the same value by reference (for objects and arrays) or the same primitive value as the last call. React uses Object.is to compare the returned snapshots and decides whether to re-render based on that comparison .

function getSnapshot() {
  return navigator.onLine;
}

For stores that hold mutable objects, the store implementation is responsible for returning a new immutable snapshot when data changes, and the cached last snapshot when it does not. Returning a new object on every call, even when the data has not changed, causes unnecessary re-renders .

The getServerSnapshot function

The optional getServerSnapshot function provides the snapshot to use during server rendering and during the initial client hydration. It must return the same value on the server and the client during hydration; otherwise React will detect a mismatch and the hydration will fail. If the store is browser-only and has no meaningful server value, the function should return a sensible default, such as true for an online-status indicator or 0 for a window dimension .

function getServerSnapshot() {
  // Default to online during SSR.
  return true;
}

If getServerSnapshot is omitted and the component is rendered on the server, React throws an error. This is intentional: it forces the developer to acknowledge that the store needs a server-side value .

The subscription lifecycle

When the component mounts, React calls subscribe with a callback. When the store invokes the callback, React schedules a re-render and calls getSnapshot to read the new value. If the value differs from the previous snapshot, React re-renders the component. When the component unmounts, React calls the cleanup function returned by subscribe, removing the subscription. If the subscribe function changes between renders, React unsubscribes with the old function and resubscribes with the new one .


b. Subscribing to browser APIs

The most common use case for useSyncExternalStore in a React application is subscribing to a browser API whose value changes over time. These APIs are not part of React’s state, and reading them directly during render produces stale or inconsistent values.

The online status example

The browser exposes navigator.onLine, a boolean that indicates whether the browser is connected to a network. The value changes without React’s knowledge, and the browser fires online and offline events on the window when it changes. A component that displays the connection status must subscribe to these events and read the current value .

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function getServerSnapshot() {
  return true;
}

function ChatIndicator() {
  const isOnline = useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot
  );

  return <h1>{isOnline ? 'Online' : 'Disconnected'}</h1>;
}

The subscribe function is declared outside the component, so it is stable across renders. The getSnapshot function reads the current value directly from the browser API. The getServerSnapshot returns true as a default for server rendering, since the server cannot know the user’s connection status .

The window dimensions example

Reading window.innerWidth or window.innerHeight during render is problematic because the value can change between renders, and the component will not re-render when it changes. The resize event fires when the window dimensions change, and a component can subscribe to it .

function useWindowWidth() {
  const width = useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot
  );
  return width;
}

function subscribe(callback) {
  window.addEventListener('resize', callback);
  return () => window.removeEventListener('resize', callback);
}

function getSnapshot() {
  return window.innerWidth;
}

function getServerSnapshot() {
  return 0;
}

The media query example

window.matchMedia returns a MediaQueryList object whose matches property indicates whether the query currently matches. The change event fires when the result changes. A custom hook can wrap this in the useSyncExternalStore pattern .

function useMediaQuery(query) {
  const subscribe = (callback) => {
    const mediaQuery = window.matchMedia(query);
    mediaQuery.addEventListener('change', callback);
    return () => mediaQuery.removeEventListener('change', callback);
  };

  const getSnapshot = () => window.matchMedia(query).matches;

  const getServerSnapshot = () => false;

  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

The subscribe and getSnapshot functions are defined inside the custom hook. Because they depend on query, they are recreated when query changes, and React will resubscribe to the new media query. This is the correct behavior .

The localStorage example

The localStorage API stores key-value pairs that persist across sessions. The storage event fires in other tabs or windows when a value changes. A component can subscribe to changes for a specific key .

function useLocalStorage(key, initialValue) {
  const subscribe = (callback) => {
    const handler = (e) => {
      if (e.key === key) callback();
    };
    window.addEventListener('storage', handler);
    return () => window.removeEventListener('storage', handler);
  };

  const getSnapshot = () => {
    const item = window.localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  };

  return useSyncExternalStore(subscribe, getSnapshot);
}

The storage event only fires in other tabs, not the one that made the change. This is a limitation of the browser API, not of the hook.


c. Extracting logic into custom hooks

Direct calls to useSyncExternalStore inside components are rare. The idiomatic pattern is to extract the subscription logic into a custom hook, which can then be reused across multiple components .

The useOnlineStatus hook

The online-status pattern is a canonical example. A single custom hook encapsulates the subscription and snapshot logic, and any component can use it without repeating the setup.

import { useSyncExternalStore } from 'react';

export function useOnlineStatus() {
  const isOnline = useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot
  );
  return isOnline;
}

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function getServerSnapshot() {
  return true;
}

Multiple components can now call useOnlineStatus() and each will subscribe independently. The subscription functions are shared, but React manages a separate subscription for each component that uses the hook .

The fine-grained selector pattern

For third-party stores that hold large state objects, a naive subscription re-renders every component that uses the store whenever any part of the store changes. A more advanced pattern is to combine useSyncExternalStore with a selector function, so the component only re-renders when the specific slice it depends on changes .

import { useSyncExternalStore, useCallback } from 'react';

function useStoreSelector(store, selector) {
  const subscribe = useCallback(
    (callback) => store.subscribe(callback),
    [store]
  );

  const getSnapshot = useCallback(
    () => selector(store.getState()),
    [store, selector]
  );

  return useSyncExternalStore(subscribe, getSnapshot);
}

// Component only re-renders when user.name changes.
function UserName() {
  const name = useStoreSelector(
    userStore,
    (state) => state.user.name
  );
  return <span>{name}</span>;
}

The selector must be stable, either defined outside the component or memoized with useCallback. A new selector on every render causes a new snapshot on every render, defeating the optimization .

The store integration pattern

A third-party store that is designed to work with useSyncExternalStore provides subscribe and getSnapshot methods, or a helper that returns them. A common pattern is a base class or factory function that manages the store’s internal state and listener list .

// A store implementation that supports useSyncExternalStore.
class Store {
  constructor(initialState) {
    this.state = initialState;
    this.listeners = new Set();
  }

  subscribe = (listener) => {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  };

  getSnapshot = () => {
    return this.state;
  };

  setState = (newState) => {
    this.state = newState;
    this.listeners.forEach((listener) => listener());
  };
}

export const todosStore = new Store({ todos: [] });

Components then use the store directly:

function TodosApp() {
  const { todos } = useSyncExternalStore(
    todosStore.subscribe,
    todosStore.getSnapshot
  );
  // ...
}

This pattern centralizes the subscription logic in the store, so components remain simple .

The Suspense caveat

React’s documentation explicitly warns against suspending a render based on a value returned by useSyncExternalStore. Store mutations cannot be marked as non-blocking Transitions, so a suspension triggered by a store change replaces already-visible content with a loading spinner, which is a poor user experience. Components that need to load data based on a store value should load the data as part of the store’s own state, rather than suspending the component .


Complete Example Session

// ============================================
// PART 1: THE PROBLEM — useEffect + useState
// ============================================

// A naive implementation of an online-status indicator.
// Works in simple cases, but is prone to tearing.

import { useState, useEffect } from 'react';

function NaiveOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    function handleOnline() { setIsOnline(true); }
    function handleOffline() { setIsOnline(false); }

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <h1>{isOnline ? 'Online' : 'Disconnected'}</h1>;
}

// Problems:
// 1. The initial state is hardcoded to true, which may not
//    match the actual value.
// 2. The subscription is set up after the first render, so
//    a change between render and effect is missed.
// 3. In concurrent rendering, the component can render with
//    a stale value if the store changes mid-render.


// ============================================
// PART 2: THE CORRECT APPROACH — useSyncExternalStore
// ============================================

import { useSyncExternalStore } from 'react';

function OnlineStatus() {
  const isOnline = useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot
  );

  return <h1>{isOnline ? 'Online' : 'Disconnected'}</h1>;
}

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function getServerSnapshot() {
  return true;
}

// The value is read synchronously during render.
// The subscription is managed by React.
// The server snapshot prevents hydration mismatches.


// ============================================
// PART 3: EXTRACTING THE LOGIC
// ============================================

// The same pattern as a reusable custom hook.

function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot
  );
}

// Now any component can use it.
function StatusBar() {
  const isOnline = useOnlineStatus();
  return <div>Status: {isOnline ? 'Online' : 'Offline'}</div>;
}

function ChatHeader() {
  const isOnline = useOnlineStatus();
  return <h2>Chat {isOnline ? 'connected' : 'disconnected'}</h2>;
}


// ============================================
// PART 4: SUBSCRIBING TO WINDOW DIMENSIONS
// ============================================

function useWindowWidth() {
  return useSyncExternalStore(
    subscribeToResize,
    getWidthSnapshot,
    getServerWidthSnapshot
  );
}

function subscribeToResize(callback) {
  window.addEventListener('resize', callback);
  return () => window.removeEventListener('resize', callback);
}

function getWidthSnapshot() {
  return window.innerWidth;
}

function getServerWidthSnapshot() {
  return 0;
}

function ResponsiveComponent() {
  const width = useWindowWidth();

  return (
    <div>
      Window width: {width}px
      {width < 768 && <p>Mobile layout</p>}
      {width >= 768 && <p>Desktop layout</p>}
    </div>
  );
}


// ============================================
// PART 5: SUBSCRIBING TO A THIRD-PARTY STORE
// ============================================

// A simple external store.
let todos = [{ id: 1, text: 'Learn React' }];
let listeners = new Set();

const todosStore = {
  subscribe(callback) {
    listeners.add(callback);
    return () => listeners.delete(callback);
  },

  getSnapshot() {
    return todos;
  },

  addTodo(text) {
    todos = [...todos, { id: Date.now(), text }];
    listeners.forEach((listener) => listener());
  },
};

function TodoList() {
  const todos = useSyncExternalStore(
    todosStore.subscribe,
    todosStore.getSnapshot
  );

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

function AddTodoButton() {
  return (
    <button onClick={() => todosStore.addTodo('New todo')}>
      Add Todo
    </button>
  );
}


// ============================================
// PART 6: THE SELECTOR PATTERN
// ============================================

// Only re-render when the selected slice changes.

function useStoreSelector(store, selector) {
  const subscribe = useCallback(
    (callback) => store.subscribe(callback),
    [store]
  );

  const getSnapshot = useCallback(
    () => selector(store.getSnapshot()),
    [store, selector]
  );

  return useSyncExternalStore(subscribe, getSnapshot);
}

// A component that only needs the count.
function TodoCount() {
  const count = useStoreSelector(
    todosStore,
    (state) => state.length
  );
  return <span>{count} todos</span>;
}


// ============================================
// PART 7: SERVER RENDERING SUPPORT
// ============================================

// The getServerSnapshot function provides the initial value
// during server rendering and hydration.

function useOnlineStatusSSR() {
  return useSyncExternalStore(
    subscribe,
    getSnapshot,
    () => true  // Server always assumes online.
  );
}

// During SSR, the HTML contains "Online".
// During hydration, the client reads the real value.
// If the real value is "Online", hydration succeeds.
// If the real value is "Offline", React updates after hydration.


// ============================================
// PART 8: WHAT NOT TO DO
// ============================================

// Wrong: Inline subscribe function without memoization.
function BadComponent() {
  const value = useSyncExternalStore(
    (callback) => {
      window.addEventListener('resize', callback);
      return () => window.removeEventListener('resize', callback);
    },
    () => window.innerWidth
  );
  // The subscribe function is new on every render.
  // React unsubscribes and resubscribes on every render.
}

// Wrong: getSnapshot returning a new object every call.
function AlsoBad() {
  const value = useSyncExternalStore(
    subscribe,
    () => ({ width: window.innerWidth })  // New object every time!
  );
  // React sees a different value every render and re-renders endlessly.
}


// ============================================
// PART 9: THE TRANSITION CONSISTENCY BEHAVIOR
// ============================================

// If the store changes during a Transition, React ensures
// all components see the same version.

import { useTransition } from 'react';

function TransitionExample() {
  const [isPending, startTransition] = useTransition();
  const isOnline = useOnlineStatus();

  function handleClick() {
    startTransition(() => {
      // If the store changes during this transition,
      // React will re-check the snapshot before committing.
      // If it changed, the update becomes blocking to
      // prevent tearing.
    });
  }

  return (
    <button onClick={handleClick} disabled={isPending}>
      {isOnline ? 'Online' : 'Offline'}
    </button>
  );
}


// ============================================
// PART 10: WHEN TO USE useSyncExternalStore
// ============================================

// USE useSyncExternalStore FOR:
// - Browser APIs that expose mutable values (navigator.onLine, window size, media queries)
// - Third-party state management libraries that hold state outside React
// - Any external data source with a subscribe/getSnapshot contract
// - When you need SSR support for external data

// DO NOT USE useSyncExternalStore FOR:
// - State that lives inside React (use useState or useReducer)
// - Simple one-time reads (use a plain variable or useMemo)
// - List keys (use data-derived keys)
// - Anything that can be expressed with React state

// The hook exists for the boundary between React and the outside world.

The ten parts progressed from the naive useEffect + useState approach through the correct useSyncExternalStore implementation, custom hook extraction, browser API subscriptions, third-party store integration, the selector pattern, server rendering support, common mistakes, and the Transition consistency behavior. The same online-status and todo-store scenarios ran through every part, showing how the hook solves the tearing and hydration problems that arise when React components read from external sources.


Quick Reference

useSyncExternalStore API

ParameterTypeDescription
subscribe(callback) => unsubscribeSubscribes to the store, returns cleanup
getSnapshot() => SnapshotReturns current store value
getServerSnapshot() => Snapshot (optional)Returns server-side value
ReturnsSnapshotCurrent store value for render

Common Use Cases

APIsubscribegetSnapshot
navigator.onLineonline/offline eventsnavigator.onLine
window.innerWidthresize eventwindow.innerWidth
matchMediachange event.matches
localStoragestorage eventgetItem(key)
Third-party storestore.subscribestore.getSnapshot

What useSyncExternalStore Solves

ProblemuseState + useEffectuseSyncExternalStore
TearingPossiblePrevented
SSR hydrationManual sync requiredgetServerSnapshot
Subscription lifecycleManual managementReact-managed
Concurrent renderingUnsafeSafe
Transition consistencyNot guaranteedGuaranteed

Custom Hook Pattern

StepAction
1Define subscribe and getSnapshot outside component
2Wrap in a custom hook
3Add getServerSnapshot if SSR is used
4Export the hook for reuse

Best Practices

✅ Do This:

// Define subscribe and getSnapshot outside the component.
function subscribe(callback) { ... }  // ✅
function getSnapshot() { ... }  // ✅

function Component() {
  const value = useSyncExternalStore(subscribe, getSnapshot);  // ✅
}

// Extract into a reusable custom hook.
export function useOnlineStatus() {
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);  // ✅
}

// Provide getServerSnapshot for SSR.
function getServerSnapshot() { return true; }  // ✅

// Memoize subscribe if it depends on props.
const subscribe = useCallback(
  (callback) => store.subscribe(id, callback),  // ✅
  [id]
);  // ✅

// Use a selector for fine-grained updates.
const name = useStoreSelector(store, (s) => s.user.name);  // ✅

// Keep getSnapshot pure and fast.
function getSnapshot() { return navigator.onLine; }  // ✅

❌ Don’t Do This:

// Don't define subscribe inline without memoization.
useSyncExternalStore(
  (cb) => { ... },  // ❌ New function every render
  () => window.innerWidth
);

// Don't return a new object from getSnapshot every call.
function getSnapshot() {
  return { width: window.innerWidth };  // ❌ Infinite re-renders
}

// Don't use useSyncExternalStore for React state.
const [count, setCount] = useState(0);
const deferred = useSyncExternalStore(...);  // ❌ Use useState

// Don't suspend based on useSyncExternalStore values.
const data = use(fetch(useSyncExternalStore(...)));  // ❌ Bad UX

// Don't omit getServerSnapshot in SSR apps.
useSyncExternalStore(subscribe, getSnapshot);  // ❌ Throws on server

Common Pitfalls

PitfallWhy It HappensFix
Infinite re-rendersgetSnapshot returns new object each callReturn cached snapshot when unchanged
Re-subscribes every rendersubscribe defined inlineDeclare outside or use useCallback
Hydration mismatchMissing or incorrect getServerSnapshotProvide a value that matches server output
Tearing still occursReading store outside the hookUse the hook for all store reads
Selector not workingSelector recreated every renderMemoize the selector
useSyncExternalStore undefinedReact < 18Upgrade to React 18+
Store changes ignoredsubscribe does not call callbackEnsure callback is invoked on change
Suspense fallback flashesSuspending on store valueLoad data in store, not via Suspense

Real-World Examples

1. Online Status Indicator

const isOnline = useSyncExternalStore(subscribe, getSnapshot, () => true);

2. Window Width Responsive Hook

const width = useSyncExternalStore(
  (cb) => { window.addEventListener('resize', cb); return () => window.removeEventListener('resize', cb); },
  () => window.innerWidth,
  () => 0
);

3. Media Query Hook

const isMobile = useSyncExternalStore(
  (cb) => { const mq = matchMedia('(max-width: 768px)'); mq.addEventListener('change', cb); return () => mq.removeEventListener('change', cb); },
  () => matchMedia('(max-width: 768px)').matches,
  () => false
);

4. LocalStorage Sync

const value = useSyncExternalStore(
  (cb) => { window.addEventListener('storage', cb); return () => window.removeEventListener('storage', cb); },
  () => localStorage.getItem('key')
);

5. Third-Party Store Integration

const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

6. Selector-Based Subscription

const count = useStoreSelector(store, (s) => s.todos.length);

7. Server Snapshot for Online Status

function getServerSnapshot() { return true; }

8. Stable Subscribe with Props

const subscribe = useCallback(
  (cb) => store.subscribe(id, cb),
  [id]
);

9. Custom Store Class

class Store {
  subscribe = (cb) => { this.listeners.add(cb); return () => this.listeners.delete(cb); };
  getSnapshot = () => this.state;
}

10. Preventing Re-subscription

// Define outside component.
function subscribe(cb) { window.addEventListener('resize', cb); return () => window.removeEventListener('resize', cb); }

Visual

The Tearing Problem

┌────────────────────────────────────────────────────────────┐
│  TEARING: INCONSISTENT READS DURING CONCURRENT RENDER      │
│                                                            │
│  Store value: A                                             │
│                                                            │
│  Component A starts rendering.                              │
│    Reads store → A.                                         │
│    Pauses (concurrent rendering).                           │
│                                                            │
│  Store changes: A → B.                                      │
│                                                            │
│  Component B renders.                                       │
│    Reads store → B.                                         │
│                                                            │
│  Component A resumes.                                       │
│    Reads store → B.                                         │
│    But its previous read was A.                             │
│                                                            │
│  Result: Component A and Component B show different         │
│  values from the same store. The UI is torn.                │
│                                                            │
│  useSyncExternalStore prevents this by reading the          │
│  store synchronously during render and ensuring all         │
│  components see the same version.                           │
│                                                            │
└────────────────────────────────────────────────────────────┘

The Hook Lifecycle

┌────────────────────────────────────────────────────────────┐
│  useSyncExternalStore LIFECYCLE                             │
│                                                            │
│  MOUNT                                                      │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. React calls subscribe(callback).              │     │
│  │  2. React calls getSnapshot() → initial value.    │     │
│  │  3. Component renders with the snapshot.          │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  STORE CHANGE                                               │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. Store invokes callback.                       │     │
│  │  2. React calls getSnapshot() → new value.        │     │
│  │  3. If Object.is(new, old) is false: re-render.   │     │
│  │  4. If Object.is(new, old) is true: skip.         │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  UNMOUNT                                                    │
│  ┌──────────────────────────────────────────────────┐     │
│  │  React calls the cleanup function from subscribe. │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  SUBSCRIBE CHANGE (e.g., deps changed)                     │
│  ┌──────────────────────────────────────────────────┐     │
│  │  React calls old cleanup, then new subscribe.     │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
└────────────────────────────────────────────────────────────┘

Browser API Subscription Pattern

┌────────────────────────────────────────────────────────────┐
│  BROWSER API SUBSCRIPTION                                   │
│                                                            │
│  Component                                                  │
│  ┌──────────────────────────────────────────────────┐     │
│  │  const isOnline = useSyncExternalStore(           │     │
│  │    subscribe,      ──────┐                        │     │
│  │    getSnapshot,    ────┐ │                        │     │
│  │    getServerSnapshot   │ │                        │     │
│  │  );                    │ │                        │     │
│  └────────────────────────┼─┼────────────────────────┘     │
│                           │ │                              │
│                           │ │                              │
│  ┌────────────────────────▼─▼────────────────────────┐     │
│  │  getSnapshot()                                    │     │
│  │    return navigator.onLine;                       │     │
│  │                                                   │     │
│  │  subscribe(callback)                              │     │
│  │    window.addEventListener('online', callback);   │     │
│  │    window.addEventListener('offline', callback);  │     │
│  │    return () => { ... };                          │     │
│  └──────────────────────────────────────────────────┘     │
│                           │                                │
│                           ▼                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Browser                                          │     │
│  │    navigator.onLine changes → online/offline event│     │
│  │    callback fires → React re-reads snapshot       │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
└────────────────────────────────────────────────────────────┘

Selector Pattern for Fine-Grained Updates

┌────────────────────────────────────────────────────────────┐
│  SELECTOR: ONLY RE-RENDER WHEN SLICE CHANGES               │
│                                                            │
│  Store: { user: { name, email }, todos: [...], ui: {...} } │
│                                                            │
│  Component A: useStoreSelector(store, s => s.user.name)    │
│    Re-renders only when user.name changes.                  │
│                                                            │
│  Component B: useStoreSelector(store, s => s.todos.length) │
│    Re-renders only when todos.length changes.               │
│                                                            │
│  Store update: user.email changes.                          │
│    Component A: user.name unchanged → skip.                 │
│    Component B: todos.length unchanged → skip.              │
│                                                            │
│  Store update: todos gets new item.                         │
│    Component A: user.name unchanged → skip.                 │
│    Component B: todos.length changed → re-render.           │
│                                                            │
│  Without selectors, both components re-render on every      │
│  store change. Selectors achieve fine-grained reactivity.   │
│                                                            │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
useSyncExternalStoreHook for subscribing to external data sources
subscribeFunction that subscribes to the store, returns cleanup
getSnapshotFunction that returns the current store value
getServerSnapshotOptional function for SSR and hydration
TearingInconsistent reads across components during concurrent render
Return stabilitygetSnapshot must return the same reference if unchanged
Custom hooksThe idiomatic way to use the hook
Selector patternFine-grained subscriptions for large stores
SSR supportProvided by getServerSnapshot
Transition behaviorFalls back to blocking if store changes mid-transition

Key takeaways:

  • useSyncExternalStore solves tearing in concurrent rendering. Reading an external store with useEffect + useState can produce inconsistent UI when React pauses and resumes renders. The hook reads the store synchronously during render and guarantees all components see the same version.
  • The hook is for external data, not React state. When possible, use useState or useReducer. The hook is specifically for data that lives outside React and has its own subscription mechanism.
  • Three functions define the contract. subscribe sets up the subscription, getSnapshot reads the current value, and getServerSnapshot provides the value for server rendering. All three must be correct for the hook to work reliably.
  • getSnapshot must return a stable reference when the store is unchanged. Returning a new object on every call causes infinite re-renders. For mutable stores, return a cached snapshot when data has not changed.
  • subscribe should be stable across renders. Defining it inline without memoization causes React to unsubscribe and resubscribe on every render. Declare it outside the component or wrap it in useCallback.
  • Custom hooks are the idiomatic pattern. Rather than calling useSyncExternalStore directly in every component, extract the subscription logic into a reusable hook like useOnlineStatus or useWindowWidth.
  • Selectors enable fine-grained updates. For large stores, wrapping useSyncExternalStore with a selector function ensures that a component only re-renders when the specific slice it depends on changes.
  • SSR requires getServerSnapshot. Omitting it causes an error on the server. The server snapshot must match the initial client render during hydration to avoid mismatches.
  • The hook guarantees Transition consistency. If a store changes during a non-blocking Transition, React falls back to a blocking update to ensure all components reflect the same store version.

Remember: useSyncExternalStore is the React solution to the problem of integrating external data sources into a concurrent rendering model. It exists because reading mutable data outside React is unsafe under concurrent rendering, and every state management library that wanted to work with React had to solve this problem on its own. The hook provides a standardized contract: subscribe to the store, getSnapshot to read the current value, and optionally getServerSnapshot for server rendering. Use it for browser APIs like online status and window dimensions, and for third-party stores that hold state outside React. Extract the logic into custom hooks so components stay simple. The hook is narrow and specific, but it is the correct tool for the boundary between React and the rest of the world.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!