DOM

useResizeObserver

Measure an element's size reactively via ResizeObserver.

Demo

drag my corner ↘
width
height

Reports element size without a window resize — the values track both the preset buttons and the native drag handle.

Usage

use-resize-observer.tsxtsx
import { useRef } from "react";
import { useResizeObserver } from "hookli";

export function Demo() {
  const boxRef = useRef<HTMLDivElement>(null);
  const { width, height } = useResizeObserver(boxRef);

  return (
    <div ref={boxRef}>
      {width === undefined ? "Measuring…" : `${Math.round(width)} × ${Math.round(height ?? 0)}`}
    </div>
  );
}

API

useResizeObserver.d.tsts
useResizeObserver<T extends HTMLElement = HTMLElement>(ref: RefObject<T>, options?: UseResizeObserverOptions): ResizeObserverSize

Parameters

NameTypeDefaultDescription
refRefObject<T>Ref to the element to measure. The observer attaches to ref.current.
optionsUseResizeObserverOptions{}Which box to measure and an optional resize callback.

Returns

NameTypeDescription
widthnumber | undefinedThe element's measured width; undefined until the first observed layout.
heightnumber | undefinedThe element's measured height; undefined until the first observed layout.

UseResizeObserverOptions

Configures the underlying ResizeObserver.

NameTypeDescription
boxResizeObserverBoxOptionsWhich box model to measure: content-box, border-box or device-pixel-content-box.
onResize(size: ResizeObserverSize) => voidCalled with the freshly measured size on every resize.

ResizeObserverSize

The size the hook returns; both values are undefined until the first measurement.

NameTypeDescription
widthnumber | undefinedMeasured width in pixels.
heightnumber | undefinedMeasured height in pixels.

Hook

The implementation, straight from hookli. Copy it, or install the package.

src/hooks/use-resize-observer/use-resize-observer.tstsx
import { RefObject, useEffect, useRef, useState } from "react";

interface ResizeObserverSize {
  width: number | undefined;
  height: number | undefined;
}

interface UseResizeObserverOptions {
  box?: ResizeObserverBoxOptions;
  onResize?: (size: ResizeObserverSize) => void;
}

export const useResizeObserver = <T extends HTMLElement = HTMLElement>(
  ref: RefObject<T>,
  options: UseResizeObserverOptions = {},
): ResizeObserverSize => {
  const { box = "content-box" } = options;
  const [size, setSize] = useState<ResizeObserverSize>({
    width: undefined,
    height: undefined,
  });

  const onResizeRef = useRef(options.onResize);
  onResizeRef.current = options.onResize;

  const previous = useRef<ResizeObserverSize>({
    width: undefined,
    height: undefined,
  });

  useEffect(() => {
    const element = ref.current;
    if (!element) return;
    if (typeof ResizeObserver === "undefined") return;

    const observer = new ResizeObserver(([entry]) => {
      if (!entry) return;

      const boxSize =
        box === "border-box"
          ? entry.borderBoxSize
          : box === "device-pixel-content-box"
            ? entry.devicePixelContentBoxSize
            : entry.contentBoxSize;

      const measured = Array.isArray(boxSize) ? boxSize[0] : boxSize;
      const width = measured ? measured.inlineSize : entry.contentRect.width;
      const height = measured ? measured.blockSize : entry.contentRect.height;

      if (
        previous.current.width === width &&
        previous.current.height === height
      ) {
        return;
      }

      const next = { width, height };
      previous.current = next;
      setSize(next);
      onResizeRef.current?.(next);
    });

    observer.observe(element, { box });
    return () => {
      observer.disconnect();
    };
  }, [ref, box]);

  return size;
};

View source on GitHub

Related hooks