Skip to content

Repository files navigation

🔌 useSyncExternalStore — connect external state to React, the right way

An interactive demo of React 18's useSyncExternalStore: a plain JavaScript store lives outside React, components subscribe with a selector, and updates from anywhere — a button, a timer, or the browser console — re-render every subscriber in sync, with no tearing.

▶ Live: https://usesyncexternalstore.vercel.app/

It's the primitive Redux, Zustand, and Jotai use under the hood.

The whole thing

The store is plain JS and knows nothing about React:

let state = { count: 0 };
const listeners = new Set();

const store = {
  getSnapshot: () => state,
  subscribe: (cb) => { listeners.add(cb); return () => listeners.delete(cb); },
  increment: () => {
    state = { ...state, count: state.count + 1 };  // immutable update
    listeners.forEach((l) => l());                 // notify subscribers
  },
};

A component subscribes with one hook:

function Counter() {
  const count = useSyncExternalStore(
    store.subscribe,                    // how to listen
    () => store.getSnapshot().count     // what to read (a selector)
  );
  return <span>{count}</span>;
}

That's it. React manages the subscription, reads a consistent snapshot, and re-renders when the selected value changes.

What the demo shows

  • Components A and B both read count from the same store. Click any button — including store.increment(), which sets no React state — and both update in sync. One source of truth, outside React.
  • A Ticker reads ticks, which a setInterval outside React bumps every second. Its render count climbs on its own, with zero React state driving it.
  • Selector isolation. A and B select count; the ticker changes ticks. Because their selector returns a primitive that didn't change, React skips their re-render — only the Ticker re-renders each second. Watch the render counters: A and B hold steady while the Ticker's climbs.
  • Open the console and type __store.increment() — the UI still updates. The store doesn't care who mutates it.

Why not just useState + useEffect?

The old pattern (subscribe in an effect, mirror the snapshot into useState) works for simple cases but can tear under React 18 concurrent rendering: different components can read the store at different moments within one render and show inconsistent values in the same frame. useSyncExternalStore guarantees every component reads the same consistent snapshot per render, forces a synchronous update when needed, and handles subscribe/unsubscribe for you. It also takes an optional getServerSnapshot for SSR.

Stack

React 19 · TypeScript · Vite.

Run locally

npm install
npm run dev

License

MIT © 2026 dev48vdev48v.infy.uk

About

Interactive demo of React 18's useSyncExternalStore — a plain JS store outside React, components subscribe with a selector, and updates from anywhere (button, timer, console) re-render every subscriber in sync with no tearing. The primitive Redux/Zustand use. React 19 + TS + Vite.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages