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.
| 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 (they never hydrate) |
| 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,
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.
Related
require-stable-server-snapshotmakes suregetServerSnapshotdoes not callmatchMedia.no-window-render-branchreportstypeof window.matchMediachecks.- Screen size and media queries read during render: the cause and its fixes
- Theme hydration mismatches, where
prefers-color-schemeis the media query