Skip to content
React rc hooks 4 min read

useSyncExternalStore

useSyncExternalStore is a React Hook designed for reading and subscribing to data that lives outside of React — browser APIs like navigator.onLine, third-party state libraries, or your own hand-rolled stores. It exists because concurrent rendering can pause, resume, and discard work, and naive useEffect-based subscriptions can show inconsistent (torn) values during that process. This Hook gives React enough information to keep every component in sync with the source of truth on every render.

Why external stores need a dedicated Hook

Before this Hook, a common pattern was to read a value into state and update it from an effect. That works in synchronous React, but concurrent features such as startTransition and useDeferredValue allow React to render in the background and interrupt that work. If the external value changes mid-render, different components can read different versions of it — a glitch known as tearing. useSyncExternalStore forces a consistent read by re-checking the snapshot synchronously before committing, so the UI never displays a mismatched mixture of old and new data.

The API and its three arguments

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
ArgumentTypePurpose
subscribe(callback) => unsubscribeRegisters a callback that the store calls whenever its data changes. Returns a cleanup function.
getSnapshot() => valueReturns the current value of the store. Must return a cached, referentially stable value when nothing changed.
getServerSnapshot() => valueOptional. Returns the initial value during server-side rendering and hydration.

Two rules make or break this Hook. First, getSnapshot must return the same reference when the data has not changed — returning a fresh object/array on every call causes an infinite render loop. Second, subscribe should be a stable function (defined outside the component or memoized), otherwise React re-subscribes on every render.

Returning { ... } or [ ... ] directly from getSnapshot is the most common mistake. Cache the value and compare it yourself, or store primitives.

A complete useOnlineStatus example

The browser exposes connectivity through navigator.onLine and the online / offline window events. This is a textbook external store: a value plus an event to subscribe to.

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; // assume online on the server
}

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

Because getSnapshot returns a boolean primitive, identity is never a concern. Now any component can consume it:

import { useOnlineStatus } from "./useOnlineStatus";

export default function StatusBar() {
  const isOnline = useOnlineStatus();
  return (
    <p>{isOnline ? "✅ Connected" : "❌ Offline — changes will sync later"}</p>
  );
}

Toggling your network in devtools updates every component using the Hook at once:

Output:

✅ Connected      // network restored
❌ Offline — changes will sync later   // network dropped

Building a custom store

For non-primitive data you control the store yourself. The pattern is a tiny pub/sub object with getSnapshot, subscribe, and mutation methods. Keep a cached snapshot so its reference only changes when the data actually changes.

function createStore(initialState) {
  let state = initialState;
  const listeners = new Set();

  return {
    getSnapshot() {
      return state;
    },
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    setState(updater) {
      const next = typeof updater === "function" ? updater(state) : updater;
      if (Object.is(next, state)) return; // skip no-op updates
      state = next;
      listeners.forEach((l) => l());
    },
  };
}

export const cartStore = createStore({ items: [], total: 0 });
import { useSyncExternalStore } from "react";
import { cartStore } from "./cartStore";

export function useCartTotal() {
  return useSyncExternalStore(
    cartStore.subscribe,
    () => cartStore.getSnapshot().total
  );
}

Note that the getSnapshot here returns state.total — a number. Returning a derived primitive sidesteps the identity trap entirely. When you genuinely need an object, only replace state with a new reference inside setState, never inside getSnapshot.

Selecting a slice of state

To subscribe to part of a larger store, derive the slice inside getSnapshot and make sure unchanged slices keep their reference. For object slices, the community helper useSyncExternalStoreWithSelector (from use-sync-external-store/with-selector) accepts a selector plus an equality function, which is what most state libraries use under the hood.

import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";

export function useCartItemCount() {
  return useSyncExternalStoreWithSelector(
    cartStore.subscribe,
    cartStore.getSnapshot,
    cartStore.getSnapshot, // server snapshot
    (state) => state.items.length // selector returns a primitive
  );
}

Best Practices

  • Reach for this Hook only for external data; for state owned by React, use useState or useReducer.
  • Define subscribe outside your component (or wrap it in useCallback) so React does not re-subscribe on every render.
  • Make getSnapshot return a referentially stable value — prefer primitives, or cache the object and update it only on real changes.
  • Always provide getServerSnapshot if your app uses server rendering, or hydration will throw a mismatch warning.
  • Wrap the Hook in a custom Hook (useOnlineStatus, useCartTotal) so the subscription wiring stays out of your components.
  • For large stores, use a selector via useSyncExternalStoreWithSelector to avoid re-rendering on unrelated changes.
Last updated June 14, 2026
Was this helpful?