Skip to solution
hardFrontend

What problem does `useSyncExternalStore` solve?

938 views
01

Understand the problem

Question presented to candidate: "What is useSyncExternalStore for, and when would you actually reach for it?"

What a strong answer should cover:

  • It is the official way to subscribe to state that lives outside React — a module store, a browser API, a websocket cache — and read it safely during rendering.
  • Two problems it solves at once: staleness (React never learns a plain variable changed) and tearing (an interruptible render reading a mutable value at two different moments).
  • The three arguments: subscribe, getSnapshot, and getServerSnapshot for SSR.
  • getSnapshot must return a cached value — a fresh object each call is an infinite loop, with React warning that the result should be cached.
  • subscribe must be stable, or React resubscribes on every render.
  • The deliberate cost: updates from an external store are synchronous and non-interruptible, which is the price of consistency.
  • You rarely write it directly — Redux, Zustand, and Jotai all call it internally. You reach for it when integrating a store or browser API yourself.
  • Genuine direct uses: matchMedia, navigator.onLine, localStorage sync across tabs, and a third-party non-React library.
  • Before React 18, libraries hand-rolled this and could tear under concurrent rendering.

Clarifying questions expected:

  • "Is the state actually outside React, or could it just be React state lifted up?"
  • "Is this server-rendered?" — that decides whether getServerSnapshot is mandatory.

Code / implementation expected: Yes — a small store with a correctly cached snapshot, and a browser-API subscription.

reactconcurrentstate-management
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for senior React interviews — assumes hooks and rendering basics. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The multi-reader result in section 3 was measured on React 19.2.8

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

A module store and a browser API, both read through the hook
Run Playground
import { useSyncExternalStore, useState, useCallback } from "react";

// ── A tiny external store. Note the snapshot is the state object itself,
//    replaced only on a write — that is what makes getSnapshot cacheable. ───
function createStore(initial) {
  let state = initial;
  const listeners = new Set();
  return {
    getSnapshot: () => state,                       // SAME reference until a write
    subscribe: (l) => { listeners.add(l); return () => listeners.delete(l); },
    setState: (patch) => {
      state = { ...state, ...patch };               // new object -> Object.is sees a change
      listeners.forEach((l) => l());
    },
  };
}

const cartStore = createStore({ items: 0 });

// WRONG, for contrast — a fresh object every call means Object.is is always
// false, so React re-renders forever:
//   getSnapshot: () => ({ items: state.items })

function CartBadge({ label }) {
  const cart = useSyncExternalStore(
    cartStore.subscribe,
    cartStore.getSnapshot,
    cartStore.getSnapshot,   // getServerSnapshot — required under SSR
  );
  return (
    <p style={{ margin: "4px 0" }}>
      <code style={{ display: "inline-block", minWidth: 110 }}>{label}</code>
      items: <strong>{cart.items}</strong>
    </p>
  );
}

// ── A browser API React knows nothing about. This is the case where you
//    genuinely write the hook yourself rather than reaching for a library. ──
const onlineStore = {
  subscribe(callback) {
    window.addEventListener("online", callback);
    window.addEventListener("offline", callback);
    return () => {
      window.removeEventListener("online", callback);
      window.removeEventListener("offline", callback);
    };
  },
  getSnapshot: () => navigator.onLine,              // a boolean — always cacheable
  getServerSnapshot: () => true,                    // assume online when rendering on the server
};

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

export default function App() {
  const isOnline = useOnlineStatus();
  const [, force] = useState(0);

  // Mutating the store from OUTSIDE React entirely — no setState involved.
  const add = useCallback(() => {
    cartStore.setState({ items: cartStore.getSnapshot().items + 1 });
  }, []);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 440 }}>
      <h4 style={{ marginTop: 0 }}>Two independent readers, one store</h4>
      <CartBadge label="header" />
      <CartBadge label="sidebar" />

      <p>
        <button onClick={add}>add an item (mutates the store directly)</button>{" "}
        <button onClick={() => force((n) => n + 1)}>re-render the app</button>
      </p>

      <p style={{ padding: 8, borderRadius: 6, background: isOnline ? "#e7f7e9" : "#fdecea" }}>
        Browser connectivity via useSyncExternalStore:{" "}
        <strong>{isOnline ? "online" : "offline"}</strong>
        <br />
        <span style={{ fontSize: 13, color: "#666" }}>
          Toggle your network — or devtools offline mode — and this updates
          without any React state involved.
        </span>
      </p>

      <p style={{ color: "#666", fontSize: 13 }}>
        Both badges always show the same number: React re-checks the snapshot
        before committing, so they cannot disagree.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 77 of 119 decoded in the React.js track. One more won't hurt.

Back to track