Rick Enciso
Rick Enciso

Reputation: 3450

How to get the width of a react element

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()

Upvotes: 162

Views: 335396

Answers (16)

Acid Coder
Acid Coder

Reputation: 2747

const MyComponent = () => {
  const [offsetWidth, setOffsetWidth] = useState(0)
  return <div ref={r=>{r && setOffsetWidth(r.offsetWidth)}}>Hello</div>
};

safer than useRef because it will rerender when div is mounted

Upvotes: 2

Florian Walther
Florian Walther

Reputation: 6961

I ended up swapping the "resize" event for a ResizeObserver because it also works when a div changes its visibility from display: none. If you use a "resize" event listener, you only get the new dimensions when the div resizes.

import { useEffect, useState } from "react";

export default function useDimensions(
  containerRef: React.RefObject<HTMLElement>,
) {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const currentRef = containerRef.current;

    const getDimensions = () => ({
      width: currentRef?.offsetWidth || 0,
      height: currentRef?.offsetHeight || 0,
    });

    // ResizeObserver instead of "resize" event, because it also works if a hidden div changes its visibility
    const resizeObserver = new ResizeObserver((entries) => {
      const entry = entries[0];
      if (entry) {
        setDimensions(getDimensions());
      }
    });

    if (currentRef) {
      resizeObserver.observe(currentRef);
      setDimensions(getDimensions());
    }

    return () => {
      if (currentRef) {
        resizeObserver.unobserve(currentRef);
      }
      resizeObserver.disconnect();
    };
  }, [containerRef]);

  return dimensions;
}

Upvotes: 1

Mulan
Mulan

Reputation: 135217

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 }

Upvotes: 23

meesern
meesern

Reputation: 2656

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/>
  )
}

Upvotes: 63

benAv
benAv

Reputation: 55

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>
        )
        }

Upvotes: 1

Marco Ant&#244;nio
Marco Ant&#244;nio

Reputation: 885

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/>
  )
}

Upvotes: 47

Dominic J. Lucenario
Dominic J. Lucenario

Reputation: 178

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;

Upvotes: 7

Fateme Norouzi
Fateme Norouzi

Reputation: 199

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>
  );
};

Upvotes: 6

Fateme Norouzi
Fateme Norouzi

Reputation: 199

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>
)

Upvotes: 1

Tony
Tony

Reputation: 583

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.

Upvotes: 4

Trevedhek
Trevedhek

Reputation: 4378

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/>
  )
}

Upvotes: 4

charri
charri

Reputation: 1052

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>
  )
}

Upvotes: 18

aptlin
aptlin

Reputation: 311

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;
}

Upvotes: 16

Nelu
Nelu

Reputation: 18710

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>;
};

Upvotes: 131

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>
  )
}

Upvotes: 3

Rick Enciso
Rick Enciso

Reputation: 3450

    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 [email protected]
        <div ref={this.myInput}>some elem</div>
        // legacy way
        // <div ref={(ref) => this.myInput = ref}>some elem</div>
        )
      }
    }

Upvotes: 143

Related Questions