Skip to main content

All hooks

useResizeObserver

Custom hook that observes the size of an element using the [`ResizeObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).


Parameters

  • ref: The ref of the element to observe.
  • onResize: When using onResize, the hook doesn't re-render on element size changes; it delegates handling to the provided callback. (default is undefined).
  • box: The box model to use for the ResizeObserver. (default is 'content-box')

Returns

  • An object with the width and height of the element if the onResize optional callback is not provided.

Polyfill

The useResizeObserver hook does not provide polyfill to give you control, but it's recommended. You can add it by re-exporting the hook like this:

// useResizeObserver.ts
import { ResizeObserver } from '@juggle/resize-observer'
import { useResizeObserver } from 'usehooks-ts'

if (!window.ResizeObserver) {
  window.ResizeObserver = ResizeObserver
}

export { useResizeObserver }

Usage

import { useRef, useState } from 'react'

import { useDebounceCallback } from '../useDebounceCallback'

import { useResizeObserver } from './useResizeObserver'

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

function useResizeObserver(options: UseResizeObserverOptions<T>): Size

Custom hook that observes the size of an element using the ResizeObserver API.

Parameters

NameTypeDefault valueDescription
optionsUseResizeObserverOptions<T>-The options for the ResizeObserver.

Returns

  • The size of the observed element.

Type declaration

Size

The size of the observed element.

NameTypeDescription
height`numberundefined`
width`numberundefined`

UseResizeObserverOptions

The options for the ResizeObserver.

NameTypeDescription
box`"border-box""content-box"
onResize(size: Size) => voidWhen using onResize, the hook doesn't re-render on element size changes; it delegates handling to the provided callback.
ref`RefObject<Tnull>`

Hook

import { useRef, useState } from 'react'
import type { RefObject } from 'react'

import { useIsMounted } from '../useIsMounted'
import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect'

/** The size of the observed element. */
export type Size = {
  /** The width of the observed element. */
  width: number | undefined
  /** The height of the observed element. */
  height: number | undefined
}

/** The options for the ResizeObserver. */
export type UseResizeObserverOptions<T extends HTMLElement = HTMLElement> = {
  /** The ref of the element to observe. */
  ref: RefObject<T | null>
  /**
   * When using `onResize`, the hook doesn't re-render on element size changes; it delegates handling to the provided callback.
   * @default undefined
   */
  onResize?: (size: Size) => void
  /**
   * The box model to use for the ResizeObserver.
   * @default 'content-box'
   */
  box?: 'border-box' | 'content-box' | 'device-pixel-content-box'
}

const initialSize: Size = {
  width: undefined,
  height: undefined,
}

/**
 * Custom hook that observes the size of an element using the [`ResizeObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
 * @template T - The type of the element to observe.
 * @param {UseResizeObserverOptions<T>} options - The options for the ResizeObserver.
 * @returns {Size} - The size of the observed element.
 * @public
 * @see [Documentation](https://usehooks-ts.com/react-hook/use-resize-observer)
 * @example
 * ```tsx
 * const myRef = useRef(null);
 * const { width = 0, height = 0 } = useResizeObserver({
 *   ref: myRef,
 *   box: 'content-box',
 * });
 *
 * <div ref={myRef}>Hello, world!</div>
 * ```
 */
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

  const observedElement = ref.current

  useIsomorphicLayoutEffect(() => {
    if (typeof window === 'undefined' || !('ResizeObserver' in window)) {
      return
    }

    let cancelled = false
    let observer: ResizeObserver | null = null
    let rafId = 0
    let pollAttempts = 0

    const teardown = () => {
      observer?.disconnect()
      observer = null
    }

    const bind = (element: T) => {
      teardown()

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

        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(element, { box })
    }

    const run = () => {
      if (cancelled) {
        return
      }

      const element = ref.current

      if (!element) {
        teardown()
        previousSize.current = { ...initialSize }
        if (!onResize.current) {
          setSize(initialSize)
        }

        if (pollAttempts++ < 32) {
          rafId = requestAnimationFrame(run)
        }
        return
      }

      pollAttempts = 0
      bind(element)
    }

    run()

    return () => {
      cancelled = true
      cancelAnimationFrame(rafId)
      teardown()
    }
  }, [box, observedElement, isMounted, ref])

  return { width, height }
}

/** @private */
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)
}