DEV Community

reactuse.com
reactuse.com

Posted on • Originally published at reactuse.com

React useResizeObserver Hook: Track Element Size Changes (2026)

React useResizeObserver Hook: Track Element Size Changes (2026)

A chart component reads its container's width once on mount and draws itself to fit. Then the user collapses the sidebar. The container just gained 300 pixels, the window never resized, no resize event fired — and the chart is now sitting in a too-wide box, drawn for a layout that no longer exists. Listening to window.resize misses every way an element can change size without the window changing: a sidebar toggling, a flex sibling appearing, content loading in above, an accordion expanding.

The browser's answer is ResizeObserver — a native API that watches an element, not the window. useResizeObserver from @reactuses/core wraps it with the lifecycle handled: observe on mount, disconnect on unmount, no stale observers left behind. Everything below is the real API, TypeScript-first.

The Manual Version, and Where It Frays

Wiring ResizeObserver into a component by hand looks simple enough:

function Chart() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!ref.current) return;
    const observer = new ResizeObserver((entries) => {
      redraw(entries[0].contentRect.width);
    });
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return <div ref={ref} />;
}
Enter fullscreen mode Exit fullscreen mode

This works — until it meets real component code. The common ways it frays:

  • The callback needs props or state. The empty dependency array means the observer keeps calling the callback from the first render. Add the dependencies and the observer is torn down and re-created on every change instead. Neither is what you want.
  • The options object re-triggers the effect. Put { box: 'border-box' } inline in the dependency array and it's a new object every render — the observer disconnects and reconnects continuously.
  • The target isn't a ref. Sometimes the element comes from a callback, a portal, or document.querySelector. The effect-plus-ref pattern needs rewiring for each shape.

None of these are hard individually. But this is lifecycle plumbing you'd be re-deriving in every component that cares about its own size — and each copy is one refactor away from a leak or an observe/disconnect loop.

useResizeObserver — The Native API, Lifecycle Handled

import { useResizeObserver } from '@reactuses/core';
import { useRef } from 'react';

function Chart() {
  const ref = useRef<HTMLDivElement>(null);

  useResizeObserver(ref, (entries) => {
    redraw(entries[0].contentRect.width);
  });

  return <div ref={ref} />;
}
Enter fullscreen mode Exit fullscreen mode

The signature:

function useResizeObserver(
  target: BasicTarget<Element>,
  callback: ResizeObserverCallback,
  options?: ResizeObserverOptions
): () => void;
Enter fullscreen mode Exit fullscreen mode

Three things worth noting, because they're where the hand-rolled version frays:

  • The target is flexible. BasicTarget accepts a ref object, a plain Element, or a getter function returning one — so the same hook covers the ref case, the querySelector case, and the "element isn't mounted yet" case (null/undefined targets are simply not observed until they exist).
  • The callback is the native ResizeObserverCallback. You get the real ResizeObserverEntry[]contentRect, borderBoxSize, contentBoxSize — not a simplified wrapper. And changing the callback between renders doesn't tear down and re-attach the observer; the hook keeps one observer per target instead of cycling it on every render.
  • Options are deep-compared. Passing { box: 'border-box' } inline is fine — the hook compares by value, not identity, so a fresh object literal each render doesn't churn the observer.

The return value is a stop() function: call it to disconnect early — say, once a measurement has been taken and you no longer care. Otherwise cleanup happens automatically on unmount.

The box Option

ResizeObserverOptions has one field, box, and it decides what "size" means:

box value What's measured
'content-box' (default) Content only — excludes padding and border
'border-box' Content + padding + border — what the element occupies in layout
'device-pixel-content-box' Content box in physical device pixels — for pixel-perfect canvas work

For most layout logic, the default is right. Reach for 'border-box' when you're matching the element's footprint in the layout, and 'device-pixel-content-box' when sizing a <canvas> backing store so it stays crisp on high-DPI screens.

Just Want the Numbers? useElementSize and useMeasure

useResizeObserver hands you raw observer entries, which is what you want for imperative work — redrawing a chart, syncing a canvas. But often the goal is simply reactive size as state. Two hooks in @reactuses/core are built directly on top of useResizeObserver for exactly that:

useElementSize — width and height as a tuple:

import { useElementSize } from '@reactuses/core';

function Card() {
  const ref = useRef<HTMLDivElement>(null);
  const [width, height] = useElementSize(ref);

  return (
    <div ref={ref}>
      {width}px × {height}px
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

It respects the same box option (and correctly sums fragmented boxes via borderBoxSize/contentBoxSize), starting at [0, 0] until the first measurement lands.

useMeasure — the full content rect, when you need more than width and height:

import { useMeasure } from '@reactuses/core';

function Panel() {
  const ref = useRef<HTMLDivElement>(null);
  const [rect, stop] = useMeasure(ref);
  // rect: { x, y, width, height, top, left, bottom, right }

  return <div ref={ref}>{rect.width}px wide</div>;
}
Enter fullscreen mode Exit fullscreen mode

If you've used the standalone react-use-measure package, this is the same idea — one dependency fewer if you're already on @reactuses/core.

Picking the Right Size Hook

The element category has several hooks that sound alike; they answer different questions:

Hook Answers Updates on
useResizeObserver "Run this code when the element resizes" Element resize (raw entries)
useElementSize "How big is this element?" Element resize
useMeasure "What's this element's content rect?" Element resize
useElementBounding "Where is this element in the viewport?" Element resize and window scroll/resize
useWindowSize "How big is the window?" Window resize

The one distinction that trips people up: useMeasure reports the observer's content rect (size-driven), while useElementBounding reports getBoundingClientRect() — viewport-relative position — and also refreshes on scroll. Positioning a tooltip or overlay? useElementBounding. Responding to size? useElementSize or useMeasure. The broader observer family — intersection, mutation, resize — is compared in React Observer Hooks.

Real Use Cases

  • Container-driven responsive components. A component that switches layout based on its own width — not the viewport's — works correctly in a sidebar, a modal, or a split pane. This is the container-query mindset, available as plain state.
  • Charts and canvas. Redraw exactly when the drawing surface changes, including sidebar toggles and flex reflows that window.resize never sees. Pair with 'device-pixel-content-box' for crisp high-DPI canvases.
  • Virtualized lists. Row measurement for variable-height virtualization: observe each row, feed real heights back to the virtualizer.
  • Auto-growing textareas and clamped text. Detect when content overflows its box and toggle "show more" affordances based on measured, not assumed, size.
  • Element-size media queries. Swap a toolbar from icons+labels to icons-only the moment its container gets tight, regardless of what the window is doing.

SSR-Safe by Construction

ResizeObserver doesn't exist on the server. All three hooks only touch it inside effects, and target resolution is guarded — during server rendering nothing runs, useElementSize returns [0, 0], useMeasure returns a zeroed rect, and hydration proceeds without a ReferenceError. No typeof window checks in your code. If you're auditing hand-rolled browser-API code for the same gap, SSR-Safe React Hooks covers the pattern.

Takeaways

  • window.resize misses most element resizes — sidebar toggles, flex reflows, content changes. ResizeObserver watches the element itself, and useResizeObserver handles its lifecycle: observe on mount, disconnect on unmount, plus a stop() for early exit.
  • The target is flexible — ref, element, or getter — and null targets are handled, so mount-order gymnastics disappear.
  • Callback and options don't churn the observer. Inline callbacks and inline options objects are fine; the hook keeps one observer per target.
  • box decides what "size" meanscontent-box by default, border-box for layout footprint, device-pixel-content-box for canvas work.
  • Want state instead of entries? useElementSize for [width, height], useMeasure for the full rect, useElementBounding when viewport position matters too.
  • SSR-safe — no observer construction on the server, no guards in your code.

Grab it from @reactuses/core and let your components respond to the size they actually have.

Top comments (1)

Collapse
 
frank_signorini profile image
Frank

This is super helpful! Have you found any specific edge cases or performance considerations when using `useResize