149

Im trying to create a range input that displays a tooltip right above the slider thumb.

I went through some vanilla JS examples online and it seems that I need to have the width of the element to acomplish that.

So I was just wondering how to get the elements width?

Pretty much the equivalent of the JQuery method $(element).width()

ischenkodv
  • 4,205
  • 2
  • 26
  • 34
Rick Enciso
  • 3,145
  • 3
  • 14
  • 14

14 Answers14

142
    class MyComponent extends Component {
      constructor(props){
        super(props)
        this.myInput = React.createRef()
      }

      componentDidMount () {
        console.log(this.myInput.current.offsetWidth)
      }

      render () {
        return (
        // new way - as of React@16.3
        <div ref={this.myInput}>some elem</div>
        // legacy way
        // <div ref={(ref) => this.myInput = ref}>some elem</div>
        )
      }
    }
Jude Niroshan
  • 4,280
  • 8
  • 40
  • 62
Rick Enciso
  • 3,145
  • 3
  • 14
  • 14
  • 8
    I just wanna say that apparently *this* way of creating refs is also deprecated now. (Now you create a ref in the constructor with `React.createRef()`) – aaaidan Apr 29 '18 at 09:38
  • 5
    @aaaidan Just checked the docs. Callback refs aren't deprecated. Sometimes you need them when you don't want to override the ref on a cloned element. – user1164937 Sep 03 '18 at 02:58
  • What id you want to get the width of Div element which is a React element not a native html element? – RezKesh May 22 '19 at 13:15
  • 2
    Thanks for maintaining the "legacy" way. – skwidbreth Sep 23 '19 at 16:07
114

With hooks:

const MyComponent = () => {
  const ref = useRef(null);
  useEffect(() => {
    console.log('width', ref.current ? ref.current.offsetWidth : 0);
  }, [ref.current]);
  return <div ref={ref}>Hello</div>;
};
Nelu
  • 16,644
  • 10
  • 80
  • 88
  • Instead of using both `useRef` and `useEffect` you can simply use a callback ref in the example above. – Izhaki Dec 16 '19 at 12:32
  • 20
    But it doesn't get updated when any resize happens, just when your ref.current changes – Marco Antônio Feb 11 '20 at 18:52
  • 12
    That works perfect. Also, with Typescript just add `const ref = useRef(null);` – Albert Tjornejoj Mar 25 '20 at 15:40
  • 4
    Eslint says: React Hook useEffect has an unnecessary dependency: 'ref.current'. Either exclude it or remove the dependency array. Mutable values like 'ref.current' aren't valid dependencies because mutating them doesn't re-render the component. – Ali Mert Çakar Sep 06 '21 at 13:14
  • 3
    I would switch to `useLayoutEffect`, and either skip the `, [ref.current]` entirely (to have it update every render), or add a `ResizeObserver` or something to update on resize. – Svish Nov 15 '21 at 11:51
59

This is basically Marco Antônio's answer for a React custom hook, but modified to set the dimensions initially and not only after a resize.

export const useContainerDimensions = myRef => {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 })

  useEffect(() => {
    const getDimensions = () => ({
      width: myRef.current.offsetWidth,
      height: myRef.current.offsetHeight
    })

    const handleResize = () => {
      setDimensions(getDimensions())
    }

    if (myRef.current) {
      setDimensions(getDimensions())
    }

    window.addEventListener("resize", handleResize)

    return () => {
      window.removeEventListener("resize", handleResize)
    }
  }, [myRef])

  return dimensions;
};

Used in the same way:

const MyComponent = () => {
  const componentRef = useRef()
  const { width, height } = useContainerDimensions(componentRef)

  return (
    <div ref={componentRef}>
      <p>width: {width}px</p>
      <p>height: {height}px</p>
    <div/>
  )
}
Kkkev
  • 4,716
  • 5
  • 27
  • 43
meesern
  • 2,518
  • 27
  • 29
  • How does this even work for anybody? myRef in the useEffect's second parameter is the same (with 'current' being null in first render) and changes, but the useEffect is not being called again because the myRef actually didn't change. – Martin. Jul 23 '23 at 20:51
49

Actually, would be better to isolate this resize logic in a custom hook. You can create a custom hook like this:

const useResize = (myRef) => {
  const [width, setWidth] = useState(0)
  const [height, setHeight] = useState(0)
  
  const handleResize = useCallback(() => {
      setWidth(myRef.current.offsetWidth)
      setHeight(myRef.current.offsetHeight)
  }, [myRef])

  useEffect(() => {
    window.addEventListener('load', handleResize)
    window.addEventListener('resize', handleResize)

    return () => {
      window.removeEventListener('load', handleResize)
      window.removeEventListener('resize', handleResize)
    }
  }, [myRef, handleResize])

  return { width, height }
}

and then you can use it like:

const MyComponent = () => {
  const componentRef = useRef()
  const { width, height } = useResize(componentRef)

  return (
    <div ref={componentRef }>
      <p>width: {width}px</p>
      <p>height: {height}px</p>
    <div/>
  )
}
syarul
  • 2,161
  • 2
  • 20
  • 22
Marco Antônio
  • 880
  • 8
  • 15
  • 1
    You have to attach the resize event listener to the window I think: https://developer.mozilla.org/en-US/docs/Web/API/Window/resize_event – Gerbus Feb 04 '20 at 19:23
  • `handleResize()` on the EventListeners or else useResize will return `{ 0, 0}` – kcNeko Feb 06 '20 at 09:23
  • 5
    Doesn't return the initial size. I.e. only works after a resize. To fix `useResize` needs to include `if (myRef.current) {) – meesern Apr 01 '20 at 19:07
  • 1
    For info, with the `addEventListener('load', ..)`it works for the initial size. – Shautieh May 12 '23 at 13:27
  • How does this even work for anybody? myRef in the useEffect's second parameter is the same (with 'current' being null in first render) and changes, but the useEffect is not being called again because the myRef actually didn't change.. – Martin. Jul 23 '23 at 20:51
18

A simple and up to date solution is to use the React React useRef hook that stores a reference to the component/element, combined with a useEffect hook, which fires at component renders.

import React, {useState, useEffect, useRef} from 'react';

export default App = () => {
  const [width, setWidth] = useState(0);
  const elementRef = useRef(null);

  useEffect(() => {
    setWidth(elementRef.current.getBoundingClientRect().width);
  }, []); //empty dependency array so it only runs once at render

  return (
    <div ref={elementRef}>
      {width}
    </div>
  )
}
Per Quested Aronsson
  • 11,380
  • 8
  • 54
  • 76
charri
  • 984
  • 7
  • 9
  • I get columnRef.current.getBoundingRect is not a function – user3808307 Aug 31 '20 at 04:08
  • 2
    that means for some reason, the ref has not been bounded to the element at first render. An easy fix for that is to add `if (!ref.current) return;` before you're using the ref and also add ref to the dependency array so it'll look like `[ref]` instead of an empty one – charri Sep 01 '20 at 16:38
  • 4
    it isn't getBoundingRect, it's getBoundingClientRect() – Douglas Schmidt Sep 14 '20 at 00:02
14

Here is a TypeScript version of @meseern's answer that avoids unnecessary assignments on re-render:

import React, { useState, useEffect } from 'react';

export function useContainerDimensions(myRef: React.RefObject<any>) {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const getDimensions = () => ({
      width: (myRef && myRef.current.offsetWidth) || 0,
      height: (myRef && myRef.current.offsetHeight) || 0,
    });

    const handleResize = () => {
      setDimensions(getDimensions());
    };

    if (myRef.current) {
      setDimensions(getDimensions());
    }

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, [myRef]);

  return dimensions;
}
aptlin
  • 191
  • 1
  • 10
7

Use this solution if all you need is exactly the title of this question: the width of a react element.

Complementing on Christopher's comment: You can use the 'react-use' library to do this. It also listens when the browser resizes. For reference: https://github.com/streamich/react-use/blob/master/docs/useMeasure.md

import React from 'react';

import { useMeasure } from 'react-use'; // or just 'react-use-measure'

const sampleView = () => {
 const [ref, { width }] = useMeasure<HTMLDivElement>();
 console.log('Current width of element', width);
 return <div ref={ref}></div>;
};

export default sampleView;
Josiah Plett
  • 180
  • 14
  • 1
    Or https://github.com/pmndrs/react-use-measure if you need only `useMeasure`. – GG. Oct 20 '21 at 18:23
6

React 18.x in 2023

For good reasons, React 18 changes how useEffect works. It's valid to run a piece of initialization code just once for a component, but read You might not need an effect before reaching for useEffect. To get an element's dimensions, we can use the new useSyncExternalStore hook -

// useDimensions.js

import { useMemo, useSyncExternalStore } from "react"

function subscribe(callback) {
  window.addEventListener("resize", callback)
  return () => {
    window.removeEventListener("resize", callback)
  }
}

function useDimensions(ref) {
  const dimensions = useSyncExternalStore(
    subscribe,
    () => JSON.stringify({
      width: ref.current?.offsetWidth ?? 0, // 0 is default width
      height: ref.current?.offsetHeight ?? 0, // 0 is default height
    })
  )
  return useMemo(() => JSON.parse(dimensions), [dimensions])
}

export { useDimensions }

You can use it like this -

function MyComponent() {
  const ref = useRef(null)
  const {width, height} = useDimensions(ref)
  return <div ref={ref}>
    The dimensions of this div is {width} x {height}
  </div>
}

why JSON.stringify?

useSyncExternalStore expects the getSnapshot function to return a cached value, otherwise it will cause infinite re-renders.

{width: 300, height: 200} === {width: 300, height: 200}
// => false ❌

JSON.stringify converts the object to a string so equality can be established -

'{"width":300,"height":200}' === '{"width":300,"height":200}'
// => true ✅

Finally, the useMemo hook ensures that the same dimensions object will be returned in subsequent renders. When the dimensions string changes, the memo is updated and the component using useDimensions will be re-rendered.

dimensions immediately available

Other answers here require the user to trigger the resize event before dimensions can be accessed. Some have attempted to mitigate the issue using a manual call inside useEffect, however these solutions fail in React 18. That is not the case for this solution using useSyncExternalState. Enjoy immediate access to the dimensions on the first render!

typescript

Here's useDimensions hook for typescript users -

import { RefObject, useMemo, useSyncExternalStore } from "react"

function subscribe(callback: (e: Event) => void) {
  window.addEventListener("resize", callback)
  return () => {
    window.removeEventListener("resize", callback)
  }
}

function useDimensions(ref: RefObject<HTMLElement>) {
  const dimensions = useSyncExternalStore(
    subscribe,
    () => JSON.stringify({
      width: ref.current?.offsetWidth ?? 0,
      height: ref.current?.offsetHeight ?? 0,
    })
  )
  return useMemo(() => JSON.parse(dimensions), [dimensions])
}

export { useDimensions }
Mulan
  • 129,518
  • 31
  • 228
  • 259
4

This could be handled perhaps in a simpler way by using callback refs.

React allows you to pass a function into a ref, which returns the underlying DOM element or component node. See: https://reactjs.org/docs/refs-and-the-dom.html#callback-refs

const MyComponent = () => {
    const myRef = node => console.log(node ? node.innerText : 'NULL!');
    return <div ref={myRef}>Hello World</div>;
 }

This function gets fired whenever the underlying node is changed. It will be null in-between updates, so we need to check for this. Example:

const MyComponent = () => {
    const [time, setTime] = React.useState(123);
    const myRef = node => console.log(node ? node.innerText : 'NULL!');
    setTimeout(() => setTime(time+1), 1000);
    return <div ref={myRef}>Hello World {time}</div>;
}
/*** Console output: 
 Hello World 123
 NULL!
 Hello World 124
 NULL!
...etc
***/

While this does't handle resizing as such (we would still need a resize listener to handle the user resizing the window) I'm not sure that is what the OP was asking for. And this version will handle the node resizing due to an update.

So here is a custom hook based on this idea:

export const useClientRect = () => {
    const [rect, setRect] = useState({width:0, height:0});
    const ref = useCallback(node => {
        if (node !== null) {
            const { width, height } = node.getBoundingClientRect();
            setRect({ width, height });
        }
    }, []);
    return [rect, ref];
};

The above is based on https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node

Note the hook returns a ref callback, instead of being passed a ref. And we employ useCallback to avoid re-creating a new ref function each time; not vital, but considered good practice.

Usage is like this (based on Marco Antônio's example):

const MyComponent = ({children}) => {
  const [rect, myRef] = useClientRect();
  const { width, height } = rect;

  return (
    <div ref={myRef}>
      <p>width: {width}px</p>
      <p>height: {height}px</p>
      {children}
    <div/>
  )
}
Trevedhek
  • 4,138
  • 2
  • 20
  • 16
4

React Hook:

import React, { useState, useEffect,useRef } from 'react';
...
const table1ref = useRef(null);
const [table1Size, table1SizeSet] = useState({
  width: undefined,
  height: undefined,
});

useEffect(() => {
    function handleResize() {
      table1SizeSet({
        width: table1ref.current.offsetWidth,
        height: table1ref.current.offsetHeight,
      });
    }
    window.addEventListener("resize", handleResize);
    handleResize();
    return () => window.removeEventListener("resize", handleResize);        
  }, [ ]);
...
<div  ref={table1ref}>

And call :

{table1Size.width}

When you want to use.

Tony
  • 493
  • 4
  • 7
4

There is a library, use-resize-observer, giving you a hook built around ResizeObserver.

import React from "react";
import useResizeObserver from "use-resize-observer";

const App = () => {
  const { ref, width, height } = useResizeObserver<HTMLDivElement>();

  return (
    <div>
      <div className="instructions">Try resizing this div!</div>
      <div ref={ref} className="box">
        {width}x{height}
      </div>
    </div>
  );
};
alexanderdavide
  • 1,487
  • 3
  • 14
  • 22
3

A good practice is listening for resize events to prevent resize on render or even a user window resize that can bug your application.

const MyComponent = ()=> {
  const myRef = useRef(null)

  const [myComponenetWidth, setMyComponentWidth] = useState('')

  const handleResize = ()=>{ 
    setMyComponentWidth(myRef.current.offsetWidth)
  }

  useEffect(() =>{
    if(myRef.current)
      myRef.current.addEventListener('resize', handleResize)

    return ()=> {
     myRef.current.removeEventListener('resize', handleResize)
    }
  }, [myRef])

  return (
  <div ref={MyRef}>Hello</div>
  )
}
  • 1
    You have to attach the resize event listener to the window I think: developer.mozilla.org/en-US/docs/Web/API/Window/resize_event – Gerbus Feb 04 '20 at 19:23
  • 1
    No you have to add this to the ref of the component that you want to know the width. Not every resize on the window, will cause the component to resize as well – Luís Guilherme Pelin Martins Jul 30 '22 at 18:17
1

here is a example in react to get the width of the a element both at page render as well when page is resized

import { useLayoutEffect, useEffect, useRef, useState } from 'react'
    const MyComponent = ()=> {    
        const ref = useRef(null)
            const [divWidth, setDivWidth] = useState('')
            const handleResize = () => {
                setDivWidth(ref.current.offsetWidth)
            }
            useEffect(() => {
                if (ref.current) window.addEventListener('resize', 
           handleResize)
        
                return () => {
                    window.removeEventListener('resize', handleResize)
                }
            }, [ref])
        
            useLayoutEffect(() => {
                setDivWidth(ref.current.offsetWidth)
            }, [])
        
        return (
             <div ref={ref} className="warper">
                <div style={{ minWidth: divWidth }}>
                    hello
                </div>
             </div>
        )
        }
benAv
  • 35
  • 5
0

custom hook

import { useCallback, useState } from "react";

const useContainerDimensions = () => {
  const [size, setRect] = useState(null);
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect());
    }
  }, []);
  return [size, ref];
}
export default useContainerDimensions;

and then you can use it like

const [size, ref] = useContainerDimensions();

return (
 <Text ellipsis style={{maxWidth: size?.width}}>
  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 
  tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 
  veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 
  commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 
  velit esse cillum dolore
 </Text>
)
  • the usage example is incomplete.`ref` is a callback that must be called with an element reference otherwise `size` will be `null`. – Mulan Jan 31 '23 at 00:22