BlogReactJS

useRef to get the width and height of an element in ReactJS

Written by Codemzy on September 2nd, 2024

In this blog post, we will `useRef` to get the width and height of a DOM element in ReactJS. We will also add an event listener to listen to the resize event for width and height changes.

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.

After React creates the DOM node and puts it on the screen, React will set the current property of your ref object to that DOM node.

- React useRef

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 use setTimeout. -- 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 />);