Sometimes you might need to know the width and/or height of an element in React.
For example, if you want a header to disappear on scroll you might need to get its height to move it off the page. Or you might be using drag and drop, and want the drop zone to be the same size as the item you are dragging.
For a simple example, let's create a box.
<div id="app"></div>
// app component function App() { return ( <div className="p-10 min-h-screen flex flex-col"> <Box /> </div> ); }; // box component function Box() { const boxRef = React.useRef(null); return ( <div ref={boxRef} className="w-full grow rounded-xl border-4 border-dashed p-5 bg-gray-50 flex items-center"> <p className="mx-auto">0px(w) x 0px(h)</p> </div> ); }; // ======================================== const root = ReactDOM.createRoot(document.getElementById('app')); root.render(<App />);
I don't know the width or height of this box. It expands to fill your screen. If you're on a desktop, it might be pretty big. If you're on a mobile, it will be smaller.
But what if I needed to know the width and height?
Well, it turns out I do need to know the width and height because I need to display it in the middle of the box! It says 0px(w) x 0px(h)
right now, which is wrong! So let's fix it.
Give the element a ref
with the useRef()
hook
To find out the width and height of the box, we will need to access the DOM node after React has rendered it on the screen. We can useRef()
for this.
First, we declare the ref object:
const boxRef = useRef(null);
And then we add the ref
attribute to our div
.
<div ref={boxRef}>
Here's our Box
component looks with the boxRef
:
// box component
function Box() {
const boxRef = React.useRef(null);
return (
<div ref={boxRef} className="...">
<p className="...">0px(w) x 0px(h)</p>
</div>
);
};
Get the width
Now that we have a ref
assigned to our element with the useRef
hook, we can access the DOM node. And that means that we can get the width using the native JavaScript method getBoundingClientRect()
.
Since we want to render the value on the page, I'll add a width state with an initial value of 0.
const [width, setWidth] = React.useState(0);
Once the component has been rendered, we will useEffect
to check the width of the element, and update the state with setWidth()
.
React.useEffect(() => {
setWidth(boxRef.current.getBoundingClientRect().width)
}, [])
And we will use that width state in our dimensions instead of 0: {width}px(w) x 0px(h)
.
Here's how the component looks now:
// box component
function Box() {
const boxRef = React.useRef(null);
const [width, setWidth] = React.useState(0);
React.useEffect(() => {
setWidth(boxRef.current.getBoundingClientRect().width)
}, [])
return (
<div ref={boxRef} className="...">
<p className="...">{width}px(w) x 0px(h)</p>
</div>
);
};
<div id="app"></div>
// app component function App() { return ( <div className="p-10 min-h-screen flex flex-col"> <Box /> </div> ); }; // box component function Box() { const boxRef = React.useRef(null); const [width, setWidth] = React.useState(0); React.useEffect(() => { setWidth(boxRef.current.getBoundingClientRect().width) }, []) return ( <div ref={boxRef} className="w-full grow rounded-xl border-4 border-dashed p-5 bg-gray-50 flex items-center"> <p className="mx-auto">{width}px(w) x 0px(h)</p> </div> ); }; // ======================================== const root = ReactDOM.createRoot(document.getElementById('app')); root.render(<App />);
On load, we get the width of the element. It doesn't stay updated if you resize the window - we will fix that later. But before that, let's get the height, and fix a little bug I hadn't noticed yet!
Get the height
Now that we know how to get the width, you might have a good idea of how to get the height. And you are probably right!
We're going to use pretty much the same code we used to get the width, but instead of getBoundingClientRect().width
we need getBoundingClientRect().height
.
We will also add a height
state and render that instead of 0!
// box component
function Box() {
const boxRef = React.useRef(null);
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
React.useEffect(() => {
let { width, height } = boxRef.current.getBoundingClientRect();
setWidth(width);
setHeight(height);
}, []);
return (
<div ref={boxRef} className="...">
<p className="...">{width}px(w) x {height}px(h)</p>
</div>
);
};
<div id="app"></div>
// app component function App() { return ( <div className="p-10 min-h-screen flex flex-col"> <Box /> </div> ); }; // box component function Box() { const boxRef = React.useRef(null); const [width, setWidth] = React.useState(0); const [height, setHeight] = React.useState(0); React.useEffect(() => { let { width, height } = boxRef.current.getBoundingClientRect(); setWidth(width); setHeight(height); }, []); return ( <div ref={boxRef} className="w-full grow rounded-xl border-4 border-dashed p-5 bg-gray-50 flex items-center"> <p className="mx-auto">{width}px(w) x {height}px(h)</p> </div> ); }; // ======================================== const root = ReactDOM.createRoot(document.getElementById('app')); root.render(<App />);
Adding setTimeout()
to run after initial render
Something I noticed after getting the height was that sometimes, in fact, most of the time, the height was wrong.
It turns out, useEffect()
may run before the browser paints the updated screen. In this case, it meant the CSS making the box full width and height of the parent container wasn't always getting picked up at the time useEffect()
checks.
React may run your Effect before the browser paints the updated screen. This ensures that the result of the Effect can be observed by the event system. Usually, this works as expected. However, if you must defer the work until after paint, such as an
alert()
, you can usesetTimeout
. -- React -useEffect
caveats
By adding a setTimeout
we can get the width and height after render to make sure they are correct.
React.useEffect(() => {
setTimeout(() => {
let { width, height } = boxRef.current.getBoundingClientRect();
setWidth(width);
setHeight(height);
}, 0);
}, []);
<div id="app"></div>
// app component function App() { return ( <div className="p-10 min-h-screen flex flex-col"> <Box /> </div> ); }; // box component function Box() { const boxRef = React.useRef(null); const [width, setWidth] = React.useState(0); const [height, setHeight] = React.useState(0); React.useEffect(() => { setTimeout(() => { let { width, height } = boxRef.current.getBoundingClientRect(); setWidth(width); setHeight(height); }, 0); }, []); return ( <div ref={boxRef} className="w-full grow rounded-xl border-4 border-dashed p-5 bg-gray-50 flex items-center"> <p className="mx-auto">{width}px(w) x {height}px(h)</p> </div> ); }; // ======================================== const root = ReactDOM.createRoot(document.getElementById('app')); root.render(<App />);
Now our width and height match what is displayed after the CSS has been applied!
Get the width/height when the element size changes
The element size might change after the initial render. For example, maybe your element width and/or height might change if the browser window is resized.
Right now, the original dimensions will remain displayed until you refresh the page because our useEffect()
only runs on the initial render.
And useRef()
doesn't trigger a re-render of your component if the element changes.
What you need to do instead is figure out what could cause the width or height of your element to change, and check for the updated values then. For this example, we can add an event listener to the useEffect()
to check for the "resize" event.
React.useEffect(() => {
// on initial render
setTimeout(() => {
updateSize();
}, 0);
// gets the size and updates state
function updateSize() {
let { width, height } = boxRef.current.getBoundingClientRect();
setWidth(width);
setHeight(height);
};
// event listener
window.addEventListener("resize", updateSize);
// remove the event listener before the component gets unmounted
return () => window.removeEventListener("resize", updateSize);
}, []);
These events can happen pretty quickly. When you resize the window that getWidth
function could run hundreds of times within seconds.
<div id="app"></div>
// app component function App() { return ( <div className="p-10 min-h-screen flex flex-col"> <Box /> </div> ); }; // box component function Box() { const boxRef = React.useRef(null); const [width, setWidth] = React.useState(0); const [height, setHeight] = React.useState(0); React.useEffect(() => { // on initial render setTimeout(() => { updateSize(); }, 0); // gets the size and updates state function updateSize() { let { width, height } = boxRef.current.getBoundingClientRect(); setWidth(width); setHeight(height); }; // event listener window.addEventListener("resize", updateSize); // remove the event listener before the component gets unmounted return () => window.removeEventListener("resize", updateSize); }, []); return ( <div ref={boxRef} className="w-full grow rounded-xl border-4 border-dashed p-5 bg-gray-50 flex items-center"> <p className="mx-auto">{width}px(w) x {height}px(h)</p> </div> ); }; // ======================================== const root = ReactDOM.createRoot(document.getElementById('app')); root.render(<App />);
If in response to getting the width and/or, you're doing something in the DOM (like changing another element to match), you might want to run it through a debounce function so that you only make the changes once when the user has finished resizing. Or you could use a throttle function to reduce the function calls during the resize.
Here's the updated code with my useDebounce
custom hook:
<div id="app"></div>
// debounce function (defaults wait to .2 seconds) const debounce = (func, wait = 200) => { let timeout; // for the setTimeout function and so it can be cleared function executedFunction(...args) { // the function returned from debounce const later = () => { // this is the delayed function clearTimeout(timeout); // clears the timeout when the function is called func(...args); // calls the function }; clearTimeout(timeout); // this clears the timeout each time the function is run again preventing later from running until we stop calling the function timeout = setTimeout(later, wait); // this sets the time out to run after the wait period }; executedFunction.cancel = function() { // so can be cancelled clearTimeout(timeout); // clears the timeout }; return executedFunction; }; // hook for using the debounce function function useDebounce(callback, delay = 1000, deps = []) { // debounce the callback const debouncedCallback = React.useCallback(debounce(callback, delay), [delay, ...deps]); // with the delay // clean up on unmount or dependency change React.useEffect(() => { return () => { debouncedCallback.cancel(); // cancel any pending calls } }, [delay, ...deps]); // return the debounce function so we can use it return debouncedCallback; }; // app component function App() { return ( <div className="p-10 min-h-screen flex flex-col"> <Box /> </div> ); }; // box component function Box() { const boxRef = React.useRef(null); const [width, setWidth] = React.useState(0); const [height, setHeight] = React.useState(0); // gets the size and updates state function updateSize() { let { width, height } = boxRef.current.getBoundingClientRect(); setWidth(width); setHeight(height); }; const debounceResize = useDebounce(function() { updateSize(); }, 200, []); React.useEffect(() => { // on initial render setTimeout(() => { updateSize(); }, 0); // event listener window.addEventListener("resize", debounceResize); // remove the event listener before the component gets unmounted return () => window.removeEventListener("resize", debounceResize); }, []); return ( <div ref={boxRef} className="w-full grow rounded-xl border-4 border-dashed p-5 bg-gray-50 flex items-center"> <p className="mx-auto">{width}px(w) x {height}px(h)</p> </div> ); }; // ======================================== const root = ReactDOM.createRoot(document.getElementById('app')); root.render(<App />);