Why useSyncExternalStore quietly became my default
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.