Measure Component Size with React and ResizeObserver

October 31, 2022

2 min read

Measure Component Size with React and ResizeObserver
Watch on YouTube

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.