useMediaQuery & useWindowSize: Controlling Responsive Design with JavaScript

Chris is building a responsive dashboard.
The designer has a specific requirement: "Please do not render the heavy sidebar component at all on mobile screens. Hiding it with display: none is a waste of performance."
CSS @media queries can control styles, but they cannot control React's Conditional Rendering.
In a rush, Chris wrote code using window.innerWidth.
// ❌ Chris's Half-Baked Responsiveness
function Dashboard() {
// Checks only once when the component first mounts.
const isMobile = window.innerWidth <= 768;
return (
<div>
{!isMobile && <Sidebar />} {/* Render only when not mobile */}
<MainContent />
</div>
);
}It works fine on PC during testing. However, when he shrinks the browser window to a mobile size, the sidebar does not disappear.
This is because there is no logic (Event Listener) to tell React, "The window size changed, so redraw!"
Today, we will create two hooks that bring responsive logic into the JavaScript realm—useWindowSize and useMediaQuery—and even optimize their performance.
1. useWindowSize: Real-Time Pixel Tracking
The first approach is to subscribe to the browser's resize event and manage the width and height as state.
// useWindowSize.ts
import { useState, useEffect } from 'react';
export function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
// Update state whenever window size changes -> Triggers re-render
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}This hook is useful when drawing on a Canvas or when complex calculations based on aspect ratio are required.
However, using this just to know "Is it mobile?" incurs a huge performance cost. While the user resizes the window, handleResize executes dozens of times per second, triggering a re-render every single time.
2. useMediaQuery: Using CSS Syntax As Is
All we want is a Yes/No answer to the question, "Is it under 768px?" We don't need to know pixel-level changes.
For this, browsers provide a powerful API called window.matchMedia.
This API allows you to use CSS media query syntax directly and fires an event only when the condition changes (e.g., crossing from 769px to 768px). It is overwhelmingly superior in terms of performance.
Implementation: useMediaQuery
// useMediaQuery.ts
import { useState, useEffect } from 'react';
export function useMediaQuery(query: string): boolean {
// For SSR: Default to false (or inject from outside)
const [matches, setMatches] = useState(false);
useEffect(() => {
// 1. Create media query matcher
const media = window.matchMedia(query);
// 2. Set initial value
if (media.matches !== matches) {
setMatches(media.matches);
}
// 3. Register event listener
const listener = () => setMatches(media.matches);
// Modern browsers: addEventListener, Older: addListener
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}Usage: Modifying Chris's Dashboard
Now you can control responsive logic in JavaScript just like writing CSS.
// Dashboard.tsx
function Dashboard() {
// ✅ Instead of tracking every 1px, it renders only when crossing the threshold.
const isMobile = useMediaQuery('(max-width: 768px)');
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
return (
<div style={{ background: isDarkMode ? '#333' : '#fff' }}>
{/* Now, simply shrinking the screen immediately Unmounts the sidebar. */}
{!isMobile && <Sidebar />}
<MainContent />
</div>
);
}3. Deep Dive: SSR and Hydration Mismatch
Caution is needed when using these hooks in an SSR environment like Next.js.
Since the server has no window, it assumes isMobile is false and renders the HTML (including the sidebar).
However, if the client is a mobile device, isMobile becomes true, and React throws a Hydration Error saying, "The server and screen are different."
There are two standard ways to solve this:
4. Which One Should I Use?
| Hook | Use Case |
| useMediaQuery | • Responsive layout placement (Mobile vs Desktop) • Detecting Dark Mode • Detecting Print Mode (This is the answer for most cases) |
| useWindowSize | • Dynamically adjusting <canvas> element size • Interactive webs needing to link scroll/mouse coordinates with window size |
Note: When using useWindowSize, you must apply Throttle or Debounce to limit the number of re-renders.
Key Takeaways
We learned how to handle the size of the screen. Now it's time to handle the "Visible Area" of the screen.
Like Facebook or Instagram, we want Infinite Scroll where new posts appear as you scroll down.
Let's create a hook that elegantly detects "Have we hit the bottom?" without complex scroll event calculations.
Continuing in: “useIntersectionObserver: The Core of Infinite Scroll and Lazy Loading”