Back to Blog
useMeasure and ResizeObserver: When Your UI Needs to Know Its Own Size
8 min readAug 8, 20265 views

useMeasure and ResizeObserver: When Your UI Needs to Know Its Own Size

Often, your UI components need to react to their own dimensions, not just window size. The `ResizeObserver` API, combined with a custom React hook like `useMeasure`, is the robust solution for this, letting components adapt dynamically without complex hacks.

ReactFrontendUIWeb DevelopmentPerformance
Share

by Sunil Band

Your UI Needs to Look in a Mirror

We build responsive UIs all the time, typically by reacting to viewport changes or using CSS media queries. That works great for global layout adjustments. But what happens when a specific component needs to know its own rendered width or height, independently of the viewport? Maybe a chart needs to redraw its axes when its container shrinks, or a dynamically sized grid needs to adjust column counts. Trying to solve this with window.resize listeners is a mess, leading to layout thrashing and poor performance. You're trying to measure something that's only indirectly related to the global window.

This is where the ResizeObserver API shines. It's a modern browser API specifically designed to notify you when an element's content rectangle changes size. It's far more efficient than polling and precisely targets the element you care about. Combining it with a custom React hook gives us a clean, declarative way to bring this power into our components.

Why Not Just getBoundingClientRect()?

You might think element.getBoundingClientRect() would be enough. And it is, if you only need the dimensions once or on demand. But getBoundingClientRect() doesn't tell you when the element's size changes. You'd have to call it repeatedly, perhaps in a setInterval or on every window.resize event, which is exactly the kind of inefficient polling ResizeObserver was built to replace.

ResizeObserver offers a declarative, push-based model. You tell the browser which elements you want to observe, and it calls your callback only when a change occurs. This is a significant performance win, as the browser can optimize when and how often these checks are performed.

Building useMeasure

Let's build a simple useMeasure hook. It will give us a ref to attach to an element and return its width and height as state. This pattern is common for many DOM-related hooks: provide a ref, get reactive state back.

First, the basic structure:

typescript
import { useState, useRef, useLayoutEffect, useCallback } from 'react';

interface Dimensions {
  width: number;
  height: number;
}

type MeasureRef = (element: HTMLElement | null) => void;

export function useMeasure(): [MeasureRef, Dimensions] {
  const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
  const elementRef = useRef<HTMLElement | null>(null);

  // ... observer logic will go here ...

  // We need a stable ref setter to pass to the user
  const setRef = useCallback((node: HTMLElement | null) => {
    elementRef.current = node;
    // Initial measurement might be needed here, or handle in effect
  }, []);

  return [setRef, dimensions];
}

Now for the ResizeObserver logic. We want to initialize the observer when the component mounts and clean it up when it unmounts. useLayoutEffect is perfect here because it runs synchronously after all DOM mutations but before the browser paints. This ensures we get the most up-to-date dimensions immediately after a render that might have changed the element's size.

typescript
import { useState, useRef, useLayoutEffect, useCallback } from 'react';

interface Dimensions {
  width: number;
  height: number;
}

type MeasureRef = (element: HTMLElement | null) => void;

export function useMeasure(): [MeasureRef, Dimensions] {
  const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
  const elementRef = useRef<HTMLElement | null>(null);
  const observerRef = useRef<ResizeObserver | null>(null); // Keep a ref to the observer

  const setRef = useCallback((node: HTMLElement | null) => {
    if (elementRef.current) {
      // Disconnect previous observation if ref changes
      observerRef.current?.disconnect();
    }
    elementRef.current = node;
    if (elementRef.current && observerRef.current) {
      // Start observing the new node
      observerRef.current.observe(elementRef.current);
      // Take an initial measurement
      const { width, height } = elementRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);

  useLayoutEffect(() => {
    // Create the observer instance
    observerRef.current = new ResizeObserver((entries) => {
      for (let entry of entries) {
        // We only care about the first observed element's dimensions
        const { width, height } = entry.contentRect;
        setDimensions({ width, height });
      }
    });

    // If an element is already set, start observing it
    if (elementRef.current) {
      observerRef.current.observe(elementRef.current);
      // Take an initial measurement here too, in case setRef wasn't called yet
      const { width, height } = elementRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }

    return () => {
      // Clean up the observer when the component unmounts
      observerRef.current?.disconnect();
      observerRef.current = null;
    };
  }, []); // Empty dependency array means this effect runs once on mount/unmount

  return [setRef, dimensions];
}

Let's break down some of the choices here:

  • useRef for elementRef and observerRef: We need stable references to the DOM element and the ResizeObserver instance that persist across renders without causing re-renders themselves. useRef is perfect for this.
  • useCallback for setRef: This is crucial. When we pass setRef to a child component (e.g., <div ref={setRef}>), we want setRef to be a stable function reference. If setRef changed on every render, it could cause unnecessary re-renders in children or break internal React optimizations.
  • useLayoutEffect: As mentioned, this ensures our measurements are based on the latest DOM state before the browser paints. This prevents visual flicker where a component might render with old dimensions then immediately update to new ones.
  • ResizeObserver callback: The callback receives an array of ResizeObserverEntry objects. Each entry corresponds to an element being observed. entry.contentRect provides the DOMRectReadOnly for the element, containing its width and height.
  • Initial Measurement: We take an initial measurement in both setRef and useLayoutEffect. This handles cases where setRef might be called before or after the useLayoutEffect runs, ensuring dimensions is never 0,0 if the element exists.
  • Cleanup: The return function in useLayoutEffect handles disconnecting the ResizeObserver. This prevents memory leaks and ensures we're not observing elements that are no longer in the DOM.

Putting useMeasure to Work

Now, how do we use this in a component? It's straightforward:

```typescript jsx
import React from 'react';
import { useMeasure } from './useMeasure'; // Assuming you put the hook in useMeasure.ts
function ResizableBox() {
const [boxRef, { width, height }] = useMeasure();
const boxStyle: React.CSSProperties = {
border: '2px solid dodgerblue',
padding: '20px',
minWidth: '100px',
minHeight: '100px',
resize: 'both', // Allows manual resizing for demo
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'aliceblue',
fontFamily: 'sans-serif',
gap: '10px'
};
return (
<div ref={boxRef} style={boxStyle}>
<h2>I'm a Resizable Box</h2>
<p>Width: {width.toFixed(2)}px</p>
<p>Height: {height.toFixed(2)}px</p>
<p>Drag me from the corner to resize!</p>
</div>
);
}
export default ResizableBox;

plaintext
plaintext
plaintext
plaintext
plaintext
plaintext

In this example, the `ResizableBox` component renders a `div` that can be manually resized using the `resize: 'both'` CSS property. As you drag the corner, the `useMeasure` hook detects the size changes, updates the `width` and `height` state, and causes the component to re-render, displaying the new dimensions. This is far more robust than trying to listen to window events or using an `iframe` hack.

### More Complex Use Cases

Beyond just displaying dimensions, `useMeasure` becomes powerful when you need to:

*   **Dynamically adjust chart scales**: A D3 or Canvas chart can redraw itself with new scales or aspect ratios when its container changes.
*   **Implement virtualized lists**: Determine how many items can fit in the viewport of a scrollable container.
*   **Create responsive grid layouts without media queries**: Adjust the number of columns or item sizes based on the container's available width, rather than the global viewport.
*   **Handle text overflow**: Calculate if text has overflowed its container and dynamically add an 'expand' button.

## Trade-offs and Gotchas

While `ResizeObserver` is a fantastic API, there are a couple of things to keep in mind:

*   **Browser Support**: It's widely supported now (all evergreen browsers), but if you need to support very old browsers (e.g., IE11), you'll need a polyfill. For most modern web development, this isn't an issue.
*   **Looping Behaviour**: If your `ResizeObserver` callback directly changes the size of the observed element, it can create an infinite loop. For example, if you read the width and then set `element.style.width = newWidth + 'px'`, that `newWidth` might trigger another resize event. The `ResizeObserver` specification explicitly addresses this by preventing infinite loops and ensuring callbacks are fired efficiently. My `useMeasure` hook only *reads* the dimensions, it doesn't modify the observed element's style, so it's safe from this particular issue.
*   **`contentRect` vs `borderBoxSize` / `devicePixelContentBoxSize`**: `entry.contentRect` gives you the size of the element's content box, excluding padding, border, and margin. Newer `ResizeObserverEntry` properties like `borderBoxSize` and `devicePixelContentBoxSize` offer more granular control over what dimensions are reported, but `contentRect` is usually sufficient for most UI layout needs and has broader browser support for these specific properties.

## Wrapping up

Stop fighting with `window.resize` or trying to guess your component's dimensions. `ResizeObserver` is the declarative, performant, and robust way to get your React components to react to their own size changes. The `useMeasure` hook pattern makes integrating this powerful API into your React applications seamless. Try adding it to a dashboard component or a custom chart in your next project. You'll immediately see the benefit of your UI components truly understanding their own space.
More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕