React Mastery / Custom Hooks

useMediaQuery Custom Hook

Building a custom useMediaQuery hook is a classic "level up" moment for any React developer. It allows you to sync your JavaScript logic with your CSS media queries, making your UI truly responsive without cluttering your components with resize listeners.

Here is a step-by-step breakdown of how to build and use this hook.


1. The Core Logic: window.matchMedia

The secret sauce is the browser's built-in window.matchMedia() API. Instead of manually calculating window.innerWidth, this API tells you if a CSS query (like (max-width: 768px)) is currently true or false.


2. Step-by-Step Implementation

Step A: Define the Hook

We start by creating a function that accepts a query string (e.g., (min-width: 1024px)).

Step B: Initialize State

We need a piece of state to track whether the media query matches. We initialize it by checking the query immediately when the hook runs.

Step C: The useEffect Setup

We need an event listener to update the state whenever the screen crosses the breakpoint.

  • Modern browsers use .addEventListener('change', ...) on the media query list.

  • Older browsers use .addListener(...).

import { useState, useEffect } from 'react';

function useMediaQuery(query) {
  // Initialize state with the current match status
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const media = window.matchMedia(query);
    
    // Update state if the initial check matches
    if (media.matches !== matches) {
      setMatches(media.matches);
    }

    // Define a listener function to update state on change
    const listener = () => setMatches(media.matches);

    // Attach the listener
    media.addEventListener('change', listener);

    // Clean up: remove the listener when the component unmounts
    return () => media.removeEventListener('change', listener);
  }, [query, matches]); // Re-run if the query string changes

  return matches;
}

export default useMediaQuery;

3. How to Use It in Your Components

Once the hook is defined, using it is incredibly clean. You can conditionally render components or change styles based on the boolean result.

import useMediaQuery from './useMediaQuery';

function MyComponent() {
  const isDesktop = useMediaQuery('(min-width: 960px)');
  const isTablet = useMediaQuery('(min-width: 600px) and (max-width: 959px)');

  return (
    <div>
      {isDesktop && <h1>Viewing on Desktop</h1>}
      {isTablet && <h1>Viewing on Tablet</h1>}
      {!isDesktop && !isTablet && <h1>Viewing on Mobile</h1>}
    </div>
  );
}

Why this is better than window.onresize

  • Performance: matchMedia is handled by the browser's engine and only fires when the "match" status changes, whereas resize fires every single time a pixel moves.

  • Consistency: It uses the exact same syntax as your CSS, ensuring your JS and CSS breakpoints stay in sync.

  • Cleanliness: The useEffect cleanup ensures you don't leave memory leaks (listeners) behind when your component disappears.