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 }