Understanding the useMediaQuery Hook for Server-Side Rendering in Material-UI
The useMediaQuery hook in Material-UI enables responsive components by detecting CSS media query matches, using default values during server-side rendering to prevent hydration mismatches before switching to live browser evaluation on the client.
The useMediaQuery hook is essential for building responsive React applications with Material-UI, allowing components to adapt their rendering based on viewport size, color scheme preferences, or other CSS media features. When implementing server-side rendering (SSR) with the mui/material-ui library, this hook requires special handling because the server environment lacks a window object to evaluate media queries. The implementation in packages/mui-material/src/useMediaQuery.ts solves this through a sophisticated dual-mode architecture that ensures deterministic server output while maintaining full responsiveness after client hydration.
How useMediaQuery Handles Server-Side Rendering
During SSR, React components execute in a Node.js environment where window.matchMedia does not exist. The hook addresses this constraint through lazy evaluation and theme integration.
Lazy Evaluation with Default Match Values
When useMediaQuery executes on the server, it cannot access real media information, so it returns a default match value to ensure the initial HTML is deterministic. This prevents the React hydration error that occurs when server and client renders produce different markup.
The hook accepts a defaultMatches option that explicitly sets the server-side value:
import useMediaQuery from '@mui/material/useMediaQuery';
function DarkModeToggle() {
// Assume dark mode on the server; client will correct if needed
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)', {
defaultMatches: true,
});
return (
<button>{prefersDark ? 'Dark Mode' : 'Light Mode'}</button>
);
}
If defaultMatches is not provided, the hook attempts to compute a sensible default based on the Material-UI theme configuration.
ThemeProvider Integration for Breakpoint Defaults
Material-UI's ThemeProvider supplies a theme object containing breakpoints that describe the application's responsive design system. The useMediaQuery hook reads this theme via useTheme from packages/mui-material/src/useTheme.ts and uses the breakpoints to infer default match values when no explicit defaultMatches is provided.
This integration ensures that server-rendered HTML aligns with the design system's default assumptions, typically rendering the mobile breakpoint on the server unless configured otherwise:
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
function ResponsiveComponent() {
const theme = useTheme();
// Uses theme breakpoints to determine default on server
const isDesktop = useMediaQuery(theme.breakpoints.up('md'));
return isDesktop ? <DesktopLayout /> : <MobileLayout />;
}
The breakpoint helpers are defined in packages/mui-material/src/styles/createBreakpoints.ts, providing the up, down, between, and only methods that generate the media query strings.
Client-Side Hydration and Media Query Listeners
Once the React application hydrates on the client, useMediaQuery transitions from static default values to live browser evaluation.
Re-evaluation After Mount
After the component mounts, the hook creates a MediaQueryList instance using window.matchMedia(query) and registers an event listener for changes. When the media query result changes (e.g., the user resizes the browser window), the hook updates its internal state, triggering a re-render with the correct responsive value.
This mechanism is implemented in the useEffect hook within packages/mui-material/src/useMediaQuery.ts:
// Simplified conceptual implementation
useEffect(() => {
const mediaQueryList = window.matchMedia(query);
const listener = (event) => {
setMatches(event.matches);
};
mediaQueryList.addEventListener('change', listener);
return () => mediaQueryList.removeEventListener('change', listener);
}, [query]);
The noSsr Option for Controlled Hydration
For scenarios where developers want to skip the server-side default entirely and wait for the client to evaluate the media query, useMediaQuery accepts a noSsr option. When set to true, the hook returns undefined (or a specified default) during the server render and initial client render, only providing the actual match value after hydration completes.
This is useful for components that should not render any content until the true viewport size is known:
import useMediaQuery from '@mui/material/useMediaQuery';
function PrintOnly() {
// Returns undefined on server, true/false only after client mount
const isPrint = useMediaQuery('print', { noSsr: true });
if (isPrint === undefined) return null;
return isPrint ? <PrintBanner /> : null;
}
Performance Optimizations in useMediaQuery
The implementation in packages/mui-material/src/useMediaQuery.ts includes several optimizations to prevent unnecessary re-renders and listener registrations.
The hook uses React.useMemo to memoize the media query string and options object, ensuring that the MediaQueryList is only recreated when the query actually changes. Additionally, the hook maintains a stable listener reference to avoid repeatedly adding and removing event listeners during component updates.
These optimizations are crucial when useMediaQuery is used in multiple components throughout an application, as they prevent the performance degradation that would occur if each hook instance created fresh media query listeners on every render.
Practical Implementation Examples
The following examples demonstrate common patterns for using useMediaQuery with server-side rendering in Material-UI applications.
Responsive Layout with Theme Breakpoints
import * as React from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
export default function ResponsiveLayout() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isDesktop = useMediaQuery(theme.breakpoints.up('md'));
return (
<Box>
{isMobile && <MobileNavigation />}
{isDesktop && <DesktopSidebar />}
<MainContent />
</Box>
);
}
Dark Mode Detection with SSR Default
import useMediaQuery from '@mui/material/useMediaQuery';
function App() {
// Server renders assuming light mode, client corrects if user prefers dark
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', {
defaultMatches: false,
});
return (
<ThemeProvider theme={prefersDarkMode ? darkTheme : lightTheme}>
<Dashboard />
</ThemeProvider>
);
}
Print Media Query with No SSR
import useMediaQuery from '@mui/material/useMediaQuery';
function InvoicePage() {
// Only evaluate on client, return null during SSR
const isPrinting = useMediaQuery('print', { noSsr: true });
if (isPrinting === undefined) {
return <StandardView />;
}
return isPrinting ? <PrintOptimizedInvoice /> : <StandardView />;
}
Summary
useMediaQueryenables responsive React components by evaluating CSS media queries, with special handling for server-side rendering environments wherewindowis unavailable.- Server-side behavior relies on
defaultMatchesor theme breakpoint inference to return deterministic values during the initial render, preventing hydration mismatches. - Client-side hydration transitions the hook to live evaluation using
window.matchMedia, with automatic state updates when viewport conditions change. - Performance optimizations include memoization of query strings and stable event listener management to prevent unnecessary re-renders.
- Configuration options like
noSsrprovide fine-grained control over when and how media queries are evaluated during the server-client transition.
Frequently Asked Questions
How does useMediaQuery prevent hydration mismatches during server-side rendering?
The hook prevents hydration mismatches by returning a deterministic default value during the server render. When running on the server, useMediaQuery cannot access window.matchMedia, so it either uses the defaultMatches option provided by the developer or infers a value from the theme's breakpoints. This ensures the HTML generated on the server matches the initial client render, allowing React to hydrate without errors. Once the component mounts on the client, the hook switches to the actual browser evaluation.
What is the difference between defaultMatches and noSsr options?
The defaultMatches option specifies the boolean value the hook should return during server-side rendering when the actual media query cannot be evaluated, ensuring deterministic output. In contrast, the noSsr option controls whether the hook should render anything at all during SSR. When noSsr is set to true, the hook returns undefined (or the default value) during both server and initial client renders, waiting until after hydration to evaluate the media query. Use defaultMatches to make assumptions about the viewport, and noSsr when you want to defer rendering entirely until the client environment is available.
How does useMediaQuery integrate with Material-UI theme breakpoints?
The hook integrates with the theme via the useTheme hook from packages/mui-material/src/useTheme.ts. When you pass a breakpoint query like theme.breakpoints.up('md'), the hook receives a media query string generated by the breakpoint system defined in packages/mui-material/src/styles/createBreakpoints.ts. If no defaultMatches option is provided, useMediaQuery examines the theme's breakpoints to infer a sensible default value for server-side rendering, typically assuming the smallest breakpoint. This tight integration ensures that responsive layouts remain consistent with the design system during both server and client execution.
Can useMediaQuery be used outside of Material-UI's ThemeProvider?
Yes, useMediaQuery can function independently of ThemeProvider, but with limitations. Without a theme context, the hook cannot infer default values from breakpoints during server-side rendering, so you must explicitly provide the defaultMatches option to avoid hydration mismatches. The hook will still create MediaQueryList listeners on the client and respond to viewport changes correctly. However, for consistent SSR behavior and access to breakpoint helpers, wrapping your application in ThemeProvider is recommended.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →