# no-match-media-in-render

> A matchMedia SSR mismatch happens when a component evaluates a media query in render. This rule reports matchMedia() calls and shows the fixes.

Source: https://hydration.jscrate.dev/docs/rules/no-match-media-in-render
Last updated: 2026-09-18

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.

| | |
| --- | --- |
| Rule | `hydration-proof/no-match-media-in-render` |
| What it reports | Disallow evaluating media queries with matchMedia while a component renders |
| recommended / next | Error |
| strict | Error |
| Server Components | Skipped with the next preset |
| Suggestions | No |
| Options | none |

## 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](https://hydration.jscrate.dev/docs/eslint#what-counts-as-render),
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:

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

## Incorrect

```jsx
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

```jsx
// 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.

## Related

- [`require-stable-server-snapshot`](https://hydration.jscrate.dev/docs/rules/require-stable-server-snapshot)
  makes sure `getServerSnapshot` does not call `matchMedia`.
- [`no-window-render-branch`](https://hydration.jscrate.dev/docs/rules/no-window-render-branch) reports
  `typeof window.matchMedia` checks.
- [Screen size and media queries read during render](https://hydration.jscrate.dev/docs/causes/media-query):
  the cause and its fixes
- [Theme hydration mismatches](https://hydration.jscrate.dev/docs/causes/theme), where
  `prefers-color-scheme` is the media query
