The component takes only render prop, a function that receives a callback to set the target element and size of that element. We are using state instead ref for storing the HTML element so that we'll get a re-render on the value change. The component is a simple wrapper, and all the action is happening in the useElementSize hook.
import { ReactNode, useState } from "react"
import { ElementSize, useElementSize } from "./hooks/useElementSize"
interface ElementSizeAwareRenderParams {
size: ElementSize | null
setElement: (element: HTMLElement | null) => void
}
interface Props {
render: (params: ElementSizeAwareRenderParams) => ReactNode
}
export const ElementSizeAware = ({ render }: Props) => {
const [element, setElement] = useState<HTMLElement | null>(null)
const size = useElementSize(element)
return <>{render({ setElement, size })}</>
}
The size interface has only the width
and height
. We can also take the element position from the bounding box, but it won't be reliable since ResizeObserver
will only tell us about size changes. We pass only one argument to the ResizeObserver
, a function to trigger when size changes. Here we extract width and height from the bounding box, check if they changed, and update the size state. We pass an element to observe to the observe method and call the disconnect method to stop listening for changes.
import { pick } from "lib/shared/utils/pick"
import { useState } from "react"
import { useIsomorphicLayoutEffect } from "react-use"
export interface ElementSize {
width: number
height: number
}
const toElementSize = (rect: DOMRect): ElementSize =>
pick(rect, "height", "width")
const areEqualSizes = (one: ElementSize, another: ElementSize) =>
one.width === another.width && one.height === another.height
export const useElementSize = (element: HTMLElement | null) => {
const [size, setSize] = useState<ElementSize | null>(() =>
element ? toElementSize(element.getBoundingClientRect()) : null
)
useIsomorphicLayoutEffect(() => {
if (!element) return
const handleElementChange = () => {
const newSize = toElementSize(element.getBoundingClientRect())
if (size && areEqualSizes(newSize, size)) return
setSize(newSize)
}
handleElementChange()
if (!window?.ResizeObserver) return
const resizeObserver = new ResizeObserver(handleElementChange)
resizeObserver.observe(element)
return () => {
resizeObserver.disconnect()
}
}, [element])
return size
}
To update the size state after the dom has changed, we are using the useLayoutEffect
hook because the useEffect
will wait until the rerender, and we might end up with flickers.