Skip to content

Instantly share code, notes, and snippets.

@jancassio
Created March 20, 2023 02:14
Show Gist options
  • Save jancassio/c48d3d52f4c72b692caef2f203c6f2f6 to your computer and use it in GitHub Desktop.
Save jancassio/c48d3d52f4c72b692caef2f203c6f2f6 to your computer and use it in GitHub Desktop.
React hook to trigger updates when a media queries matches or not its query.

useMatchMedia hook

Allow to obtain match responses of given media query list items.

Details

This hook relies on window.matchMedia to provide precise updates when some media query matches or not its requirements. It will also handle and dispose event listeners properly and avoid uncessary re-renders.

API

useMatchMedia(mediaQueries: string[]): boolean[]

Params

  • mediaQueries: a list of media queries to match for.

Return

  • boolean[]: Each index matches its media query index in mediaQueries param.

Usage

export function UseMatchMedia(): JSX.Element {
  const [mobile, tablet, desktop] = useMatchMedia([
    "(max-width: 767px)",
    "(min-width: 768px) and (max-width: 1023px)",
    "(min-width: 1024px)",
  ]);

  return (
    <div>
      <h1>useMatchMedia</h1>
      <p>
        useMatchMedia is a custom hook that returns a boolean value based on the
        media query passed to it.
      </p>
      <p>It is useful for creating responsive components.</p>
      <p>It is a wrapper around the window.matchMedia() method.</p>
      <div>
        <h2>Example</h2>
        <ul>
          <li>
            Mobile: <span>{mobile ? "Yes" : "No"}</span>
          </li>
          <li>
            Tablet: <span>{tablet ? "Yes" : "No"}</span>
          </li>
          <li>
            Desktop: <span>{desktop ? "Yes" : "No"}</span>
          </li>
        </ul>
      </div>
    </div>
  );
}

Gotcha

At first render, it will update twice because it needs to wait for dom elements and window scope be ready, so at first render all elements returned by the hook will always be false. The immediate next state change, will provide the right values for the media queries.

License

MIT

/*
useMatchMedia.ts
MIT License
Copyright (c) 2023 Jan Cassio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { useCallback, useEffect, useRef, useState } from "react";
type MediaQuery = string;
/**
* Listen to media queries updates and return if they match or not.
*
* @example
* const [isSmall, isMedium, isLarge] = useMatchMedia([
* "(max-width: 600px)",
* "(min-width: 601px) and (max-width: 900px)",
* "(min-width: 901px)",
* ]);
*
*
* @param queries Array of media queries to listen to.
* @returns An array of booleans that match the media queries.
*/
export const useMatchMedia = (queries: MediaQuery[]): boolean[] => {
const [matches, setMatches] = useState(() => queries.map(() => false));
// stable value type to avoid unnecessary rerenders
const value = JSON.stringify(queries);
// persist media query list to unlisten on unmount or value change
const mediaQueryLists = useRef<MediaQueryList[]>([]);
const listener = useCallback(
(event: MediaQueryListEvent) => {
const index = mediaQueryLists.current.findIndex(
(mql) => mql.media === event.media
);
setMatches((prev) => {
const next = [...prev];
next[index] = event.matches;
return next;
});
},
[value]
);
const dispose = useCallback(() => {
mediaQueryLists.current.forEach((mql) =>
mql.removeEventListener("change", listener)
);
}, [value]);
useEffect(() => {
// remove previous listeners
dispose();
queries.forEach((query, index) => {
const mql = window.matchMedia(query);
// updates the state immediately at first component render
// React will batch the updates
setMatches((prev) => {
const next = [...prev];
next[index] = mql.matches;
return next;
});
mql.addEventListener("change", listener);
mediaQueryLists.current.push(mql);
});
return () => {
dispose();
};
}, [value]);
return matches;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment