useSyncExternalStore

Subscribe a component to any store outside React — safely, without tearing, and SSR-ready.

useSyncExternalStore is the official bridge between React and state that lives outside React: a Redux/Zustand store, a browser API like navigator.onLine or matchMedia, an event emitter, or a WebSocket. It is what state libraries use under the hood, and it is the correct way to read external mutable data during concurrent rendering.

Why not just useState + useEffect?

The naive approach — read the value in an effect and mirror it into state — can tear: during a concurrent render, different components can momentarily read different versions of the same source. useSyncExternalStore guarantees every component in a render sees one consistent snapshot.

The three arguments

  • subscribe(callback) — register callback to run whenever the store changes; return an unsubscribe function.
  • getSnapshot() — return the current value. Must return a stable value (same reference for unchanged data), or you will loop.
  • getServerSnapshot() (optional) — the value to use during server rendering / hydration.
const isOnline = useSyncExternalStore(
  (cb) => {
    window.addEventListener('online', cb);
    window.addEventListener('offline', cb);
    return () => {
      window.removeEventListener('online', cb);
      window.removeEventListener('offline', cb);
    };
  },
  () => navigator.onLine,          // client snapshot
  () => true                       // server snapshot
);

Wrap that in a useOnlineStatus custom hook and you have a clean, tear-free, SSR-safe primitive the rest of your app can reuse.

Example

Example · javascript
import { useSyncExternalStore } from 'react';

// A minimal external store, fully outside React.
function createStore(initial) {
  let state = initial;
  const listeners = new Set();
  return {
    getState: () => state,
    setState: (next) => {
      state = typeof next === 'function' ? next(state) : next;
      listeners.forEach((l) => l());
    },
    subscribe: (l) => {
      listeners.add(l);
      return () => listeners.delete(l);
    },
  };
}

const counterStore = createStore(0);

// A selector-style hook so components subscribe to just what they read.
function useCounter() {
  return useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getState,
    () => 0 // server snapshot
  );
}

function Display() {
  const count = useCounter();
  return <p>Count: {count}</p>;
}

function Controls() {
  // Note: does NOT read the store, so it never re-renders on change.
  return (
    <button onClick={() => counterStore.setState((c) => c + 1)}>
      Increment
    </button>
  );
}

When to use it

  • A Redux integration uses useSyncExternalStore internally to subscribe components to the Redux store so they re-render safely without tearing during concurrent renders.
  • A browser online/offline indicator subscribes to window's 'online' and 'offline' events using useSyncExternalStore to give React a safe, tear-free read of navigator.onLine.
  • A custom in-memory event bus shared between React and a legacy jQuery section uses useSyncExternalStore so React components stay in sync with events fired outside the tree.

More examples

Subscribe to navigator.onLine

Bridges the browser's online/offline events into React with useSyncExternalStore, including an SSR snapshot.

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);
  };
}

const getSnapshot = () => navigator.onLine;
const getServerSnapshot = () => true; // assume online on SSR

function NetworkStatus() {
  const online = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
  return <p>{online ? 'Online' : 'Offline'}</p>;
}

Subscribe to a custom store

Implements the minimal subscribe/getSnapshot interface that useSyncExternalStore requires from any external store.

Example · js
// miniStore.js — a tiny observable store
let state = { count: 0 };
const listeners = new Set();

export const store = {
  getSnapshot: () => state,
  subscribe: (cb) => { listeners.add(cb); return () => listeners.delete(cb); },
  increment: () => {
    state = { count: state.count + 1 };
    listeners.forEach(cb => cb());
  },
};

Use the custom store in a component

Connects the component to the custom store with useSyncExternalStore, which handles subscription and tear-free reads.

Example · jsx
import { useSyncExternalStore } from 'react';
import { store } from './miniStore';

function Counter() {
  const { count } = useSyncExternalStore(store.subscribe, store.getSnapshot);
  return (
    <>
      <p>Count: {count}</p>
      <button onClick={store.increment}>+</button>
    </>
  );
}

Discussion

  • Be the first to comment on this lesson.