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 }