Last active
September 13, 2021 04:54
-
-
Save loburets/459de25eef578d72782c48ee607d37ea to your computer and use it in GitHub Desktop.
React hook to check element sizes and do it in 1 render cycle so in the next one they can be covered by preloader/sekeleton component using the sizes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useEffect, useRef } from 'react' | |
/** | |
* This hook provides an array of refs with a fixed length. | |
* Then you can set the refs to any of your elements and on the next render tick, you will now size of such elements. | |
* | |
* Example of usage: | |
* | |
* const reservedQuantityOfElements = 2 | |
* const [elementsToBeSized, elementsSizes, temporaryShowElementsAsIs] = useElementsSizes(reservedQuantityOfElements) | |
* | |
* // here you can use elementsSizes[0] and elementsSizes[1] to see width, height and margins for the 2 divs below | |
* | |
* return ( | |
* <div ref={elementsToBeSized[0]}> | |
* <div ref={elementsToBeSized[1]}> | |
* </div> | |
* </div> | |
* ) | |
* | |
* "temporaryShowElementsAsIs" is an optional boolean variable. | |
* It will be true during one render cycle to let components show the element in the original state. | |
* It can be helpful, for example, for the case when the element will be hidden by a preloader, | |
* but to know the size of the preloader, we need to render the component 1 time without it. | |
*/ | |
export default function useElementsSizes (quantityOfElementsWithSkeletons) { | |
const elementsToBeSized = [] | |
for (let i = 0; i < quantityOfElementsWithSkeletons; i++) { | |
elementsToBeSized[i] = useRef(null) | |
} | |
const [temporaryShowElementsAsIs, setTemporaryShowElementsAsIs] = useState(false) | |
const [isSizesMeasured, setIsSizesMeasured] = useState(false) | |
const [elementsSizes, setElementsSizes] = useState([]) | |
useEffect(_ => { | |
if (typeof window === 'undefined') { | |
return | |
} | |
// When these conditions are satisfied, it is already the third render. | |
// All the job of this hook has been already done | |
// So we don't need to have all the measured elements anymore, now they can be hidden or changed if required | |
if (isSizesMeasured && temporaryShowElementsAsIs) { | |
setTemporaryShowElementsAsIs(false) | |
} | |
// It is the third render or some of the following renders of the component | |
// But all the sizes are already defined, so nothing to be done in this hook and we just exit it | |
if (isSizesMeasured) { | |
return | |
} | |
// The first render, so the hook will let the component know that it should temporarily show the elements if | |
// they were hidden or changed someway. For example, the elements could be changed by "skeleton" loaders. | |
// In this case, the skeletons will be temporarily disabled for one render circle to let the hook | |
// measure the original size. | |
if (!temporaryShowElementsAsIs) { | |
setTemporaryShowElementsAsIs(true) | |
return | |
} | |
const sizes = [] | |
// It is a second render, so if some elements were not ready to be measured, | |
// they already changed by the previous render and we can measure them now | |
elementsToBeSized.forEach((element, index) => { | |
if (!element.current) { | |
return | |
} | |
const style = getComputedStyle(element.current) | |
sizes[index] = { | |
height: element.current.offsetHeight, | |
width: element.current.offsetWidth, | |
marginBottom: parseFloat(style.marginBottom) | |
} | |
}) | |
setElementsSizes(sizes) | |
setIsSizesMeasured(true) | |
}, [temporaryShowElementsAsIs, isSizesMeasured]) | |
return [elementsToBeSized, elementsSizes, temporaryShowElementsAsIs] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example how it can be used to adjust skeleton sizes for the content: