-
-
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 | |
| 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
useEffectis 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, andcolumnscan be madeconst.By the way love your https://usehooks.com/ site