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 | |
2 | |
); | |
// 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 | |
columns[shortColumnIndex].push(item); | |
// Update height | |
columnHeights[shortColumnIndex] += item.height; | |
}); | |
// Render columns and items | |
return ( | |
<div className="App"> | |
<div className="columns is-mobile"> | |
{columns.map(column => ( | |
<div className="column"> | |
{column.map(item => ( | |
<div | |
className="image-container" | |
style={{ | |
// Size image container to aspect ratio of image | |
paddingTop: (item.height / item.width) * 100 + '%' | |
}} | |
> | |
<img src={item.image} alt="" /> | |
</div> | |
))} | |
</div> | |
))} | |
</div> | |
</div> | |
); | |
} | |
// Hook | |
function useMedia(queries, values, defaultValue) { | |
// Array containing a media query list for each query | |
const mediaQueryLists = queries.map(q => window.matchMedia(q)); | |
// Function that gets value based on matching media query | |
const getValue = () => { | |
// 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; | |
}; | |
// 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)); | |
}, | |
[] // Empty array ensures effect is only run on mount and unmount | |
); | |
return value; | |
} |
@rluiten Yeah the stying is probably weird. Probably left-over from original source code I copied at https://codesandbox.io/s/26mjowzpr?from-embed. 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!
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 = queries.map(q => 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;
}
`
Why does this not use useLayoutEffect
since it is likely based on layout changes?
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).
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" ? queries.map(q => window.matchMedia(q)) : [];
I offer a more simple form of the hook that only accepts a single query:
function useMediaQuery(query) {
const [matches, setMatches] = useState(false);
useEffect(
() => {
const mediaQuery = window.matchMedia(query);
// Update the state with the current value
setMatches(mediaQuery.matches);
// 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)'),
};
}
^ Looks like Safari handles listeners for matchMedia
differently. It's addListener
on Safari and not addEventListener
Tested on Safari 13.1.3
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
, andcolumns
can be madeconst
.By the way love your https://usehooks.com/ site