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 (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 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-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