pmndrs/react-use-measure
> A React hook that measures a DOM node's bounding rect and re-reports it on resize and scroll.
GitHub repo · npm · License: MIT
Overview
react-use-measure is a small single-purpose hook from the Poimandres (pmndrs) collective — the same group behind react-three-fiber, zustand, and react-spring. It returns a functional ref and a bounds object; attach the ref to any element and bounds gives you that element's x, y, width, height, top, right, bottom, and left. Unlike a one-shot getBoundingClientRect() call, the hook keeps the values current by subscribing to a ResizeObserver and, optionally, to scroll events on the element's ancestor scroll containers and the window1.
The problem it targets is narrow but real: getBoundingClientRect() returns coordinates relative to the viewport at the instant it is called, and it ignores the offsets contributed by nested scroll areas. There is no built-in reactive equivalent — you cannot ask the platform to notify you when an element's measured position changes2. This hook wires up the observers and listeners so consuming components re-render with fresh geometry, which is what layout-driven UI (tooltips, popovers, drag targets, virtualized lists, canvas overlays, animated measurements) actually needs.
The defining tradeoff is scope discipline. The library does exactly one thing and does not try to be a general "element size + position + intersection" toolkit. That keeps it tiny and predictable, but it also means callers must understand ResizeObserver semantics — first-run zero values, layout-thrash costs of reading geometry, and the fact that scroll: true attaches listeners to every scrollable ancestor.
Getting Started
npm install react-use-measure
# or: yarn add react-use-measure / pnpm add react-use-measure
import useMeasure from 'react-use-measure'
function App() {
const [ref, bounds] = useMeasure()
// bounds is all-zero on the first render — the element hasn't been
// measured yet. You get real numbers after layout, via a re-render.
return (
<div ref={ref}>
{bounds.width} x {bounds.height}
</div>
)
}
With options — debounce measurements and track nested scroll offsets:
const [ref, bounds] = useMeasure({
debounce: { scroll: 50, resize: 0 },
scroll: true, // re-measure when ancestor scroll areas move
offsetSize: true, // use offsetWidth/Height to ignore parent CSS transforms
})
Architecture / How It Works
The hook holds a ResizeObserver instance and a set of DOM event listeners, wired through a functional (callback) ref so it can run setup on mount and teardown on unmount without a useEffect cleanup race1.
- Measurement source. By default it calls
getBoundingClientRect()on the observed node and copies the eightRectReadOnlyfields into state. WithoffsetSize: trueit substitutesoffsetWidth/offsetHeightforwidth/height, which sidesteps the fact thatgetBoundingClientRect()returns the transformed box — useful when a parent appliesscale()and you want the untransformed size. - Resize reactivity. A
ResizeObserverfires on any size change of the observed element. This is the primary trigger and covers the common "my box resized, update the number" case. - Scroll reactivity. When
scroll: true, the hook walks up the ancestor chain, finds scrollable containers, and attaches passive scroll listeners to each plus the window. Position fields (top,left, etc.) are relative to the viewport, so they only change on scroll — which is why scroll tracking is opt-in and off by default. - Debouncing.
debounceaccepts either a single number or{ scroll, resize }, letting you throttle the two event sources independently. Scroll fires far more often than resize, so a scroll-only debounce is the typical configuration. - Ref identity. The returned ref is the hook's own functional ref — it is not a mutable ref object you created. This is deliberate: unmount detection depends on the callback ref being invoked with
null.
RectReadOnly is a plain readonly object, not a live DOMRect. Each update produces a new state value, so referential equality changes on every measurement — relevant if you feed bounds into a useEffect/useMemo dependency array.
Production Notes
- First render is always zero. Every field is
0until the element has been measured post-layout. Guard against dividing bybounds.width, and expect one extra render on mount. This surprises people building layout math that assumes synchronous measurement. ResizeObserveravailability. The hook depends onResizeObserver, which is present in all current browsers but absent in jsdom and older environments. In SSR the first client render matches the server's zeros, then hydrates and measures — usually fine, but content that shifts on measurement can cause a visible reflow. For tests or legacy targets, inject a polyfill via thepolyfilloption; the README recommends@juggle/resize-observer1.scroll: trueis not free. It attaches listeners to every scrollable ancestor. On deeply nested layouts this is more work than people expect, and each scroll event triggers agetBoundingClientRect()read. Debounce scroll, and leavescrolloff when the element's position is effectively static.- Layout thrash. Reading
getBoundingClientRect()forces the browser to flush pending layout. Measuring many elements per frame (e.g., one hook per row in a long list) can become a bottleneck; prefer measuring a container and computing offsets, or use a virtualization library that batches reads. - Transforms. By default measured
width/heightreflect CSS transforms becausegetBoundingClientRect()does. If a parent scales the subtree and you want the intrinsic size, setoffsetSize: true. - Bring your own ref. Because the hook owns the ref, attaching your own ref to the same node requires a merge utility such as
react-merge-refs1. - Maintenance cadence. The library is small and stable rather than actively evolving; the last publish predates React 19's release, and there is a modest backlog of open issues. It works with React 18 and function components; there is no plan for feature growth, which for a utility this focused is a reasonable steady state rather than abandonment.
When to Use / When Not
Use when:
- You need an element's live size and viewport position, kept current across resize and scroll.
- You're positioning tooltips, popovers, drag overlays, or canvas layers relative to a DOM node.
- You want a tiny dependency with a clear API instead of hand-wiring
ResizeObserverand scroll listeners.
Avoid when:
- You only need size, never position — a bare
ResizeObserverhook (e.g.react-resizable-panels' internals, oruse-resize-observer) is lighter and does not touch scroll. - You need to know when an element enters/leaves the viewport — that's
IntersectionObserverterritory, not this hook. - You're measuring hundreds of nodes per frame — the per-node
getBoundingClientRect()cost will dominate; measure containers instead.
Alternatives
- ZeeCoder/use-resize-observer — thin
ResizeObserverwrapper; use when you only need width/height and no scroll-aware position. - streamich/react-use — the
useMeasurein this grab-bag covers size only; use when you already depend on the library and don't need position tracking. - juggle/resize-observer — a
ResizeObserverpolyfill, not a hook; use as thepolyfillinjection for old browsers or jsdom tests. - floating-ui/floating-ui — full positioning engine for tooltips/popovers; use instead when the end goal is anchored floating UI, not raw measurements.
- react measurement via
getBoundingClientRect()by hand — use when the measurement is one-shot (on click, on mount) and does not need to react to resize or scroll at all.
History
| Version | Date | Notes |
|---|---|---|
| initial | 2019-10 | First release; ResizeObserver-based measurement hook1. |
| 2.x | 2020–2021 | scroll, debounce, and polyfill options; TypeScript RectReadOnly types. |
| 2.1.x | 2022–2025 | offsetSize option; maintenance releases. Latest publish 2025-013. |
(Exact version-to-date mapping is not reconstructed here; consult the npm version history and git tags for precise release dates.)
References
- ^ pmndrs/react-use-measure README — usage, options, polyfill and multiple-ref notes. https://github.com/pmndrs/react-use-measure
- ^ "Retrieve the position (X,Y) of an HTML element" — Stack Overflow discussion cited by the README on why no simple reactive API exists. https://stackoverflow.com/questions/442404/retrieve-the-position-x-y-of-an-html-element
- ^ react-use-measure on npm — version history and current release. https://www.npmjs.com/package/react-use-measure
Tags
react, hooks, typescript, resize-observer, dom-measurement, bounding-rect, layout, frontend, pmndrs, ui-utilities