useSyncExternalStore

Subscribe a component to an external store and read a consistent snapshot on every render.

Signature
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
  • Added in React 18.0.0

Parameters

ParameterTypeDescription
$subscribe(callback: () => void) => () => voidA function that takes a single callback argument and subscribes it to the store. When the store changes, it should invoke the provided callback, which will cause React to re-call getSnapshot and (if needed) re-render the component. The subscribe function should return a function that cleans up the subscription.
$getSnapshot() => SnapshotA function that returns a snapshot of the data in the store that's needed by the component. While the store has not changed, repeated calls to getSnapshot must return the same value. If the store changes and the returned value is different (as compared by Object.is), React re-renders the component.
$getServerSnapshotoptional() => SnapshotA function that returns the initial snapshot of the data in the store. It is used only during server rendering and during hydration of server-rendered content on the client. The server snapshot must be the same between the client and the server, and is usually serialized and passed from the server to the client.

Return value

SnapshotThe current snapshot of the store, which you can use in your rendering logic.

Errors

Rendering the component on the server throws if getServerSnapshot is omitted, and React throws "The result of getSnapshot should be cached" when getSnapshot returns a freshly allocated value on every call.

Changelog

  • PHP 18.0.0Introduced so external stores can support concurrent reads by forcing updates to the store to be synchronous.
  • PHP 18.0.0If the store is mutated during a non-blocking Transition update, React calls getSnapshot a second time before committing and restarts the update as blocking if the value changed.

useSyncExternalStore connects a component to state that lives outside React — a store object, a third-party library, or a browser API such as navigator.onLine or matchMedia. React subscribes on mount, reads the value with getSnapshot during render, and re-renders when the snapshot changes by Object.is.

When to reach for it

Use it when the source of truth is not React state and you need reads to stay consistent under concurrent rendering. The naive alternative — useState plus a useEffect subscription — is subject to tearing: a concurrent render can be interrupted, the store mutates mid-render, and two components on screen display different versions of the same value. This Hook forces those updates to be synchronous so the whole tree agrees.

If you own the state and it is plain React data, reach for useState or context instead. This Hook is for bridging, not for general state management.

What trips people up

  • Fresh objects. If getSnapshot returns { x, y } built on each call, every call is a new reference and React throws "The result of getSnapshot should be cached". Return primitives, or cache the last object and reuse it until the data actually changes.
  • Inline subscribe. Defining it in the component body creates a new function each render, so React unsubscribes and resubscribes every time. Hoist it to module scope or wrap it in useCallback.
  • Server rendering. Omitting getServerSnapshot throws during SSR, and the value it returns must match on both sides.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Read navigator.onLine or a matchMedia query in render without a useEffect/useState pair that can tear.
  • Bind components to a third-party store (Redux, Zustand, a custom event emitter) that lives outside React.
  • Subscribe to a module-level singleton shared by several unrelated components in the tree.
  • Mirror a value written to localStorage by other tabs via the storage event.

Frequently asked questions

Why do I get "The result of getSnapshot should be cached"?

Because `getSnapshot` returns a newly allocated value — usually an object or array literal, a `.map()`, or a `.filter()` — on every call. React compares snapshots with `Object.is`, sees a difference each time, and loops. Return a primitive, or memoize the derived value and return the same reference until the underlying data changes.

Should I use useSyncExternalStore instead of useState?

No, not for state you own. This Hook is for bridging state that lives outside React: a third-party store, a module singleton, or a browser API. For component state, `useState` or `useReducer` is simpler and already safe under concurrent rendering.

Why does my subscribe function run after every render?

Because it is defined inside the component, so it is a new function on each render and React resubscribes. Move `subscribe` to module scope, put it on the store object, or wrap it in `useCallback` with the right dependencies.

More examples

Subscribing to a browser API

The heading reads "Online" and flips to "Disconnected" the moment the browser fires an `offline` event. The third argument matters here: `navigator` does not exist on the server, so without `getServerSnapshot` the SSR render throws. Returning `true` there means the markup ships as "Online" and corrects itself after hydration.

Example · jsx
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 useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // client snapshot
    () => true              // server snapshot: assume online
  );
}

export default function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? "Online" : "Disconnected"}</h1>;
}

Caching the snapshot to avoid the infinite loop

The list starts showing only "Ship release". Clicking the button rewrites `todos`, the cache key misses, and a new filtered array is produced exactly once. The commented-out version returns a fresh array on every call, so `Object.is` never matches, React re-renders forever and reports "The result of getSnapshot should be cached".

Example · jsx
import { useSyncExternalStore } from "react";

let todos = [
  { id: 1, text: "Write docs", done: true },
  { id: 2, text: "Ship release", done: false },
];
let cachedVisible = null;
let cachedSource = null;
const listeners = new Set();

function subscribe(callback) {
  listeners.add(callback);
  return () => {
    listeners.delete(callback);
  };
}

// WRONG: a new array every call -> "The result of getSnapshot should be cached"
// function getSnapshot() {
//   return todos.filter((t) => !t.done);
// }

// RIGHT: recompute only when the source array actually changed.
function getSnapshot() {
  if (cachedSource !== todos) {
    cachedSource = todos;
    cachedVisible = todos.filter((t) => !t.done);
  }
  return cachedVisible;
}

function toggleFirst() {
  todos = todos.map((t, i) => (i === 0 ? { ...t, done: !t.done } : t));
  listeners.forEach((listener) => listener());
}

export default function TodoList() {
  const visible = useSyncExternalStore(subscribe, getSnapshot);
  return (
    <div>
      <ul>
        {visible.map((t) => (
          <li key={t.id}>{t.text}</li>
        ))}
      </ul>
      <button onClick={toggleFirst}>Toggle first todo</button>
    </div>
  );
}

Wrapping a store in a reusable Hook

This is the shape most store libraries expose. The paragraph shows "light" and toggles to "dark" on click, and every component calling `useStore(themeStore)` updates in the same commit. `subscribe` and `getState` are methods on a long-lived object, so their identity is stable across renders and React never tears down the subscription; passing `getState` as `getServerSnapshot` works because the initial state is the same on both sides.

Example · jsx
import { useSyncExternalStore } from "react";

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

  return {
    getState: () => state,
    setState(updater) {
      state = typeof updater === "function" ? updater(state) : updater;
      listeners.forEach((listener) => listener());
    },
    // Declared once, on the store object, so the identity never changes.
    subscribe(callback) {
      listeners.add(callback);
      return () => {
        listeners.delete(callback);
      };
    },
  };
}

const themeStore = createStore("light");

function useStore(store) {
  return useSyncExternalStore(store.subscribe, store.getState, store.getState);
}

export default function ThemeSwitcher() {
  const theme = useStore(themeStore);
  return (
    <div>
      <p>Current theme: <strong>{theme}</strong></p>
      <button
        onClick={() =>
          themeStore.setState((t) => (t === "light" ? "dark" : "light"))
        }
      >
        Toggle theme
      </button>
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.