useResizeObserver
Custom hook that observes the size of an element using the ResizeObserver API.
Usage
import { useRef, useState } from 'react'
import { useDebounceCallback, useResizeObserver } from 'usehooks-ts'
type Size = {
  width?: number
  height?: number
}
export default function Component() {
  const ref = useRef<HTMLDivElement>(null)
  const { width = 0, height = 0 } = useResizeObserver({
    ref,
    box: 'border-box',
  })
  return (
    <div ref={ref} style={{ border: '1px solid palevioletred', width: '100%' }}>
      {width} x {height}
    </div>
  )
}
export function WithDebounce() {
  const ref = useRef<HTMLDivElement>(null)
  const [{ width, height }, setSize] = useState<Size>({
    width: undefined,
    height: undefined,
  })
  const onResize = useDebounceCallback(setSize, 200)
  useResizeObserver({
    ref,
    onResize,
  })
  return (
    <div
      ref={ref}
      style={{
        border: '1px solid palevioletred',
        width: '100%',
        resize: 'both',
        overflow: 'auto',
        maxWidth: '100%',
      }}
    >
      debounced: {width} x {height}
    </div>
  )
}
API
▸ useResizeObserver<T>(options): Size
Custom hook that observes the size of an element using the ResizeObserver API.
Type parameters
| Name | Type | Description | 
|---|---|---|
| T | extends HTMLElement=HTMLElement | The type of the element to observe. | 
Parameters
| Name | Type | Description | 
|---|---|---|
| options | UseResizeObserverOptions<T> | The options for the ResizeObserver. | 
Returns
- The size of the observed element.
Type aliases
Ƭ Size: Object
The size of the observed element.
Type declaration
| Name | Type | Description | 
|---|---|---|
| height | number|undefined | The height of the observed element. | 
| width | number|undefined | The width of the observed element. | 
Ƭ UseResizeObserverOptions<T>: Object
The options for the ResizeObserver.
Type parameters
| Name | Type | 
|---|---|
| T | extends HTMLElement=HTMLElement | 
Type declaration
| Name | Type | Description | 
|---|---|---|
| box? | "border-box"|"content-box"|"device-pixel-content-box" | The box model to use for the ResizeObserver. Defaultts 'content-box'  | 
| onResize? | ( size:Size) =>void | When using onResize, the hook doesn't re-render on element size changes; it delegates handling to the provided callback.Defaultts undefined  | 
| ref | RefObject<T> | The ref of the element to observe. | 
Hook
import { useEffect, useRef, useState } from 'react'
import type { RefObject } from 'react'
import { useIsMounted } from 'usehooks-ts'
type Size = {
  width: number | undefined
  height: number | undefined
}
type UseResizeObserverOptions<T extends HTMLElement = HTMLElement> = {
  ref: RefObject<T>
  onResize?: (size: Size) => void
  box?: 'border-box' | 'content-box' | 'device-pixel-content-box'
}
const initialSize: Size = {
  width: undefined,
  height: undefined,
}
export function useResizeObserver<T extends HTMLElement = HTMLElement>(
  options: UseResizeObserverOptions<T>,
): Size {
  const { ref, box = 'content-box' } = options
  const [{ width, height }, setSize] = useState<Size>(initialSize)
  const isMounted = useIsMounted()
  const previousSize = useRef<Size>({ ...initialSize })
  const onResize = useRef<((size: Size) => void) | undefined>(undefined)
  onResize.current = options.onResize
  useEffect(() => {
    if (!ref.current) return
    if (typeof window === 'undefined' || !('ResizeObserver' in window)) return
    const observer = new ResizeObserver(([entry]) => {
      const boxProp =
        box === 'border-box'
          ? 'borderBoxSize'
          : box === 'device-pixel-content-box'
            ? 'devicePixelContentBoxSize'
            : 'contentBoxSize'
      const newWidth = extractSize(entry, boxProp, 'inlineSize')
      const newHeight = extractSize(entry, boxProp, 'blockSize')
      const hasChanged =
        previousSize.current.width !== newWidth ||
        previousSize.current.height !== newHeight
      if (hasChanged) {
        const newSize: Size = { width: newWidth, height: newHeight }
        previousSize.current.width = newWidth
        previousSize.current.height = newHeight
        if (onResize.current) {
          onResize.current(newSize)
        } else {
          if (isMounted()) {
            setSize(newSize)
          }
        }
      }
    })
    observer.observe(ref.current, { box })
    return () => {
      observer.disconnect()
    }
  }, [box, ref, isMounted])
  return { width, height }
}
type BoxSizesKey = keyof Pick<
  ResizeObserverEntry,
  'borderBoxSize' | 'contentBoxSize' | 'devicePixelContentBoxSize'
>
function extractSize(
  entry: ResizeObserverEntry,
  box: BoxSizesKey,
  sizeType: keyof ResizeObserverSize,
): number | undefined {
  if (!entry[box]) {
    if (box === 'contentBoxSize') {
      return entry.contentRect[sizeType === 'inlineSize' ? 'width' : 'height']
    }
    return undefined
  }
  return Array.isArray(entry[box])
    ? entry[box][0][sizeType]
    : // @ts-ignore Support Firefox's non-standard behavior
      (entry[box][sizeType] as number)
}