~ / mitul
$
Senior Full Stack & AI Engineer
Surat, IN - UTC+05:30
OPEN TO WORK
~30 hrs/wk

Mitul Jagad

Senior Full Stack Developer. Six years shipping production for SaaS and FinTech teams. Lately building AI agents and workflow automations that have to actually run, not just demo.

Resume
FOLLOW
ALL ARTICLESReact

Why useSyncExternalStore quietly became my default

Jan 20265 min
React

Why useSyncExternalStore quietly became my default

useEffect + useState was getting me hydration mismatches every other week. useSyncExternalStore solved it cleanly - and once you see the pattern, you start using it everywhere.

If you build SSR React apps, you've hit this: a `useState` initialised from `window.matchMedia(...)` flashes the wrong value during hydration. A theme stored in `localStorage` flickers on first paint. A scroll-position-driven component renders one thing on the server and another on the client, and React yells at you in the console.

The standard fix used to be: render nothing on the server, set a `mounted` flag in `useEffect`, then render the real value once mounted. It works, but every component using a browser API needs the boilerplate, and you're paying with a flash of empty content.

useSyncExternalStore exists for exactly this

React 18 shipped `useSyncExternalStore` and the marketing was "for state-management library authors". That undersold it. It's the cleanest way to subscribe a React component to anything outside React's state graph - `matchMedia`, `localStorage`, `document.visibilityState`, scroll position, the network status - and have hydration just work.

The pattern

function useMediaQuery(query: string): boolean {
  return useSyncExternalStore(
    // Subscribe to changes
    (callback) => {
      const mql = window.matchMedia(query);
      mql.addEventListener("change", callback);
      return () => mql.removeEventListener("change", callback);
    },
    // Read the current value (client)
    () => window.matchMedia(query).matches,
    // Server snapshot (no window) - return a stable default
    () => false
  );
}

Three callbacks: how to subscribe, how to read on the client, how to read on the server. React handles the rest - including suppressing the hydration warning when the server snapshot legitimately differs from the client value.

Where I now reach for it instead of useEffect

  • Reading `prefers-reduced-motion` to gate animations.
  • Reading `(pointer: coarse)` to disable a custom cursor on touch devices.
  • Reading the persisted theme from `localStorage` without a flash of default theme.
  • Reading `document.visibilityState` to pause expensive animations when the tab is hidden.
  • Reading the active route in custom hooks that need to react to URL changes outside Next's router context.

When NOT to use it

It's a subscription primitive - wrong choice for one-shot async state (use a fetcher / `useSWR` / etc.). It's also wrong for state that React already owns (don't wrap your `useState` in it). The rule: if the source of truth lives outside React and changes over time, this is your hook.

The bigger lesson

React 18's quieter additions - `useSyncExternalStore`, `useId`, `useDeferredValue` - have aged better than the hyped ones. They solved real problems most teams were patching around with brittle custom code. Worth re-reading the release notes; you may have a couple of `useEffect`s that want to be something else.