Skip to content

Instantly share code, notes, and snippets.

Last active November 23, 2022 16:30
Show Gist options
  • Save gragland/ed8cac563f5df71d78f4a1fefa8c5633 to your computer and use it in GitHub Desktop.
Save gragland/ed8cac563f5df71d78f4a1fefa8c5633 to your computer and use it in GitHub Desktop.
import { useState, useEffect } from 'react';
function App() {
const columnCount = useMedia(
// Media queries
['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
// Column counts (relates to above media queries by array index)
[5, 4, 3],
// Default column count
// Create array of column heights (start at 0)
let columnHeights = new Array(columnCount).fill(0);
// Create array of arrays that will hold each column's items
let columns = new Array(columnCount).fill().map(() => []);
data.forEach(item => {
// Get index of shortest column
const shortColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
// Add item
// Update height
columnHeights[shortColumnIndex] += item.height;
// Render columns and items
return (
<div className="App">
<div className="columns is-mobile">
{ => (
<div className="column">
{ => (
// Size image container to aspect ratio of image
paddingTop: (item.height / item.width) * 100 + '%'
<img src={item.image} alt="" />
// Hook
function useMedia(queries, values, defaultValue) {
// State update function
const match = () => {
// Get first media query that matches
const query = queries.findIndex(q => matchMedia(q).matches);
// Return related value or defaultValue if none
return values[query] || defaultValue;
// State and setter for current value
const [value, set] = useState(match);
useEffect(() => {
// Update state on window resize
// Usage of match function defined outside of useEffect ...
// ... ensures that it has current values of arguments.
const handler = () => set(match);
window.addEventListener('resize', handler);
// Remove event listener on cleanup
return () => window.removeEventListener('resize', handler);
}, []); // Empty array ensures effect is only run on mount and unmount
return value;
Copy link

ostgals commented Jan 25, 2019

I see two possible optimizations.

The first one allows to avoid unneed rerender each time the 'resize' event is fired by calling set only if value actually changed.
And the second will save some computations and memory allocation expences by memoizing matchMedia calls (matchMedia returns an instance of MediaQueryList which can be reused between renders).

Optimized sandbox:

Copy link

@ostgals Thanks for the tips! I've actually changed this hook quite a bit. It now supports any media query, not just screen width. I think as a result the issues you pointed out are resolved, but let me know if any other suggestions (or issues with updated hook).

Copy link

rluiten commented Mar 1, 2019

This is a question about the styling, it seems to me it could be a bit simpler. I do not know how you arrived at this solution and it may be an artifact of a previous version, or maybe it behaves differently in some scenario I am not conscious of.

I noticed if I remove the inline style on .image-container (remove the padding-top) then remove all the styles for .image-container except leaving only margin-bottom it seems to be equivalent. Is this so?

Noticed something else trivial I believe the two identifiers columnHeights, and columns can be made const.

By the way love your site

Copy link

@rluiten Yeah the stying is probably weird. Probably left-over from original source code I copied at Generally, I just try to get the styling done as quickly as possible since the main focus is on the React hook recipe.. so don't go pushing my styling right to production :)

And glad you're liking the site!

Copy link

cbrannen9a commented Apr 30, 2019

The eslint exhaustive-deps rules have issues with useEffect not using dependencies think the below resolves that, does require use of useCallback hook as well now, hope this helps:


function useMedia(queries, values, defaultValue) {
// Array containing a media query list for each query
const mediaQueryLists = => window.matchMedia(q));

// Function that gets value based on matching media query
const getValue = useCallback(() => {
    // Get index of first media query that matches
    const index = mediaQueryLists.findIndex(mql => mql.matches);
    // Return related value or defaultValue if none
    return typeof values[index] !== 'undefined'
        ? values[index]
        : defaultValue;
}, [values, defaultValue, mediaQueryLists]);

// State and setter for matched value
const [value, setValue] = useState(getValue);

useEffect(() => {
    // Event listener callback
    // Note: By defining getValue outside of useEffect we ensure that it has ...
    // ... current values of hook args (as this hook callback is created once on mount).
    const handler = () => setValue(getValue);
    // Set a listener for each media query with above handler as callback.
    mediaQueryLists.forEach(mql => mql.addListener(handler));
    // Remove listeners on cleanup
    return () =>
        mediaQueryLists.forEach(mql => mql.removeListener(handler));
}, [getValue, mediaQueryLists]);

return value;



Copy link

Why does this not use useLayoutEffect since it is likely based on layout changes?

Copy link

shuhei commented Jul 17, 2019

I think the following comment is not true. The useEffect's callback captures the getValue on mount, which holds hook args on mount.

    // Note: By defining getValue outside of useEffect we ensure that it has ...
    // ... current values of hook args (as this hook callback is created once on mount).

Copy link

AleVul commented Aug 2, 2019

this hooks crashes node process if the component is server side rendered, i propose to check window object to avoid that:
const mediaQueryLists = typeof window !== "undefined" ? => window.matchMedia(q)) : [];

Copy link

justincy commented Oct 1, 2020

I offer a more simple form of the hook that only accepts a single query:

function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);
    () => {
      const mediaQuery = window.matchMedia(query);
      // Update the state with the current value
      // Create an event listener
      const handler = (event) => setMatches(event.matches);
      // Attach the event listener to know when the matches value changes
      mediaQuery.addEventListener('change', handler);
      // Remove the event listener on cleanup
      return () => mediaQuery.removeEventListener('change', handler);
    [] // Empty array ensures effect is only run on mount and unmount
  return matches;
  • I believe this is much easier to understand.
  • This hook works with SSR as-is because useEffect is not executed on the server.
  • It can be called multiple times to keep track of multiple breakpoints.
function useBreakpoints() {
  return {
    isXs: useMediaQuery('(max-width: 640px)'),
    isSm: useMediaQuery('(min-width: 641px) and (max-width: 768px)'),
    isMd: useMediaQuery('(min-width: 769px) and (max-width: 1024px)'),
    isLg: useMediaQuery('(min-width: 1025px) and (max-width: 1280px)'),
    isXl: useMediaQuery('(min-width: 1281px)'),

Copy link

nullhook commented Oct 31, 2020

^ Looks like Safari handles listeners for matchMedia differently. It's addListener on Safari and not addEventListener

Tested on Safari 13.1.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment