30% offEnding soon
useSyncExternalStoreLoading saved progress…

useSyncExternalStore

useSyncExternalStore is the React hook that subscribes a component to a store that lives outside React — a Redux store, a browser API, a plain object with listeners — and returns a consistent snapshot that re-renders the component whenever that store changes. Your job is to build the client shim: the exact userland implementation React ships as use-sync-external-store/shim for versions that predate the built-in hook. You implement it out of the smaller primitives (useState, useEffect, useLayoutEffect) — not by calling React's built-in useSyncExternalStore, which would be cheating the whole point.

The hard part is a one-frame gap. A component reads the store's value while it renders, then subscribes to the store slightly later, in an effect. If the store changes in between, a naive hook that only waits for future notifications never hears about that change — the screen shows a stale value, a bug React calls tearing. The shim closes the gap by re-reading the snapshot at the moment it subscribes.

Signature

function useSyncExternalStore<T>(
  // Registers a callback the store calls on every change; returns an unsubscribe fn.
  subscribe: (onStoreChange: () => void) => () => void,
  // Returns the store's current, immutable snapshot. Must be cached for unchanged state.
  getSnapshot: () => T,
): T // the current snapshot

Examples

// A store outside React: a value, a Set of listeners, and a notify.
const store = createStore(0); // { getSnapshot, subscribe, set }

function Counter() {
  const count = useSyncExternalStore(store.subscribe, store.getSnapshot);
  return <span>{count}</span>;
}

store.set(1); // every mounted Counter re-renders and shows 1
// The snapshot is compared with Object.is. Notifying with an equal value
// wakes nobody; only a genuinely different snapshot re-renders.
const same = { n: 1 };
const store = createStore(same);
// store.set(same)      -> Object.is(same, same) is true  -> no re-render
// store.set({ n: 1 })  -> a different reference           -> re-render

Notes

  • The render-to-subscribe gap is the whole question. Reading the snapshot during render and subscribing in an effect are two separate moments; a change in the gap must not be lost. Re-check getSnapshot() right before you subscribe.
  • Compare with Object.is, never deep equality. Re-render only when the snapshot's identity changes. An equal snapshot must not re-render.
  • Re-subscribe when subscribe changes identity. Key the subscription effect on subscribe, and clean up the previous subscription first.
  • getSnapshot must return a cached value for unchanged state. A getSnapshot that builds a fresh object each call is never Object.is-equal to itself and loops forever — that constraint is on the caller, but your hook must not paper over it.
  • Out of scope: the optional third argument getServerSnapshot (used for SSR/hydration) — the core is the client path. It appears in Going further.