Hydration Proof

Search documentation

Find a page or section

Disallow evaluating media queries with matchMedia while a component renders.

A matchMedia SSR check cannot know the visitor's screen size or preferences on the server. If the browser chooses different markup during hydration, React finds a mismatch. no-match-media-in-render flags matchMedia() in render code, including state initializers and guarded calls. Use CSS for layout, or read the query after hydration.

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 uses the server snapshot for the first browser render, then reads the real media query. That keeps the initial markup consistent. If both menus are in the HTML, hide the unused one with CSS media queries; the only-mobile and only-desktop class names above are placeholders for your app's styles.

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.