Reputation: 3450
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Reputation: 129
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
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