Hydration Proof

Search documentation

Find a page or section

Disallow evaluating media queries with matchMedia while a component renders.

A matchMedia SSR mismatch happens when a component evaluates a media query while it renders: the server has no screen and no user preferences, so it renders one branch, and the browser renders another while hydrating. no-match-media-in-render reports matchMedia() in render code, including state initializers and code behind a typeof window check.

Rulehydration-proof/no-match-media-in-render
What it reportsDisallow evaluating media queries with matchMedia while a component renders
recommended / nextError
strictError
Server ComponentsSkipped with the next preset (they never hydrate)
SuggestionsNo
Optionsnone

What it reports

matchMedia(...), window.matchMedia(...), self.matchMedia(...) and globalThis.matchMedia(...) calls (and references to matchMedia that are not a typeof check) in render code, including state initializers and code behind a typeof window check.

Why matchMedia SSR output differs from the browser

The server has no screen and no user preferences. Code that guards the call renders a fallback on the server and the real answer in the browser:

server HTML:   <nav class="menu-desktop">   (no matchMedia: assumes desktop)
client render: <nav class="menu-mobile">    (matchMedia('(max-width: 600px)').matches)

Incorrect

function Menu() {
  const mobile =
    typeof window !== "undefined" &&
    window.matchMedia("(max-width: 600px)").matches;
  return mobile ? <MobileMenu /> : <DesktopMenu />;
}
 
function useReducedMotion() {
  const [reduced] = useState(
    () => matchMedia("(prefers-reduced-motion: reduce)").matches
  );
  return reduced;
}

Correct

// Let CSS decide when possible: both menus are in the HTML.
function Menu() {
  return (
    <>
      <MobileMenu className="only-mobile" />
      <DesktopMenu className="only-desktop" />
    </>
  );
}
 
// Or subscribe with a server snapshot.
function useMediaQuery(query) {
  return useSyncExternalStore(
    (onChange) => {
      const list = window.matchMedia(query);
      list.addEventListener("change", onChange);
      return () => list.removeEventListener("change", onChange);
    },
    () => window.matchMedia(query).matches,
    () => false
  );
}

useSyncExternalStore renders the server snapshot during hydration and then switches to the real value, so hydration matches.

Options

This rule has no options.

Messages

What ESLint prints for this rule, word for word:

  • <read> evaluates a media query during render. The server has no screen or user preferences, so it renders a different branch than the browser does while hydrating. Render a neutral default and evaluate the query in useEffect or useSyncExternalStore (with getServerSnapshot), or use a CSS media query.

When not to use it

In components that are never server-rendered.