React renders elements from data, but it cannot tell you their size. Layout is a browser concern that only exists after React commits the DOM, so measuring a box, a list item, or a tooltip position requires stepping outside render with a ref. This article covers reading a node once, staying in sync as it resizes, and cleaning up.
Get a reference to the node
Declare a ref and attach it to the element you want to measure. React fills the ref during the commit phase, so read it later from an Effect or an event handler.
import { useRef } from "react";
function Card() {
const cardRef = useRef(null);
return <div ref={cardRef}>A card</div>;
}On the first render the node does not exist yet, so cardRef.current is null. After React creates the div and puts it on the screen, cardRef.current points to that DOM node.
Measure once after mount
To capture the size as soon as the element appears, read getBoundingClientRect inside an Effect. The Effect runs after commit, when the node exists and has layout.
import { useRef, useEffect, useState } from "react";
function SizedCard() {
const cardRef = useRef(null);
const [size, setSize] = useState(null);
useEffect(() => {
const rect = cardRef.current.getBoundingClientRect();
setSize({ width: rect.width, height: rect.height });
}, []);The Effect reads the rectangle after the DOM commits and stores the width and height in state. This single snapshot is all you need when the element size never changes.
return (
<>
<div ref={cardRef} style={{ width: "16rem" }}>A card</div>
{size && <p>{Math.round(size.width)} by {Math.round(size.height)} pixels</p>}
</>
);
}The page shows the measured size below the card. The empty dependency array runs the Effect once, which is correct for a single snapshot.
Stay in sync with ResizeObserver
A one-time measurement goes stale when the browser window changes size. ResizeObserver watches the node and reports every size change.
import { useRef, useEffect, useState } from "react";
function ResponsiveCard() {
const cardRef = useRef(null);
const [width, setWidth] = useState(0);
useEffect(() => {
const node = cardRef.current;
const observer = new ResizeObserver((entries) => {
setWidth(entries[0].contentRect.width);
});
observer.observe(node);
return () => observer.disconnect();
}, []);The observer starts watching the card and reports each new content width as layout changes. The returned cleanup disconnects it when the component unmounts.
return (
<>
<div ref={cardRef} style={{ width: "60%" }}>A responsive card</div>
<p>Current width: {Math.round(width)} pixels</p>
</>
);
}The width label updates as you resize the window. The cleanup calls disconnect, so the observer stops watching after the component unmounts. Without that cleanup, the observer would keep a reference to a removed node.
Measure in an event handler
Some measurements only matter in response to an interaction, and those can run directly in the handler.
import { useRef } from "react";
function Tooltip() {
const targetRef = useRef(null);
function handleClick() {
const rect = targetRef.current.getBoundingClientRect();
console.log(rect.left, rect.top);
}
return <button ref={targetRef} onClick={handleClick}>Show position</button>;
}Clicking the button reads its current position from the committed DOM. No Effect is involved because the measurement is caused by a specific user action.
Use useLayoutEffect when it must run before paint
Measurements read after paint can cause a visible flicker when you immediately use them to position something. useLayoutEffect runs after the DOM is mutated but before the browser paints, so it is the right tool for that case. See useEffect vs useLayoutEffect for when to pick each one.
What to learn next
Measurements and observers pair with a broad ref toolkit. Continue with useRef vs useState to know when a value belongs in a ref instead of state.
Rune AI
Key Insights
- Attach a ref to reach the DOM node, then read it after commit.
- getBoundingClientRect returns the element's size and position at one moment.
- ResizeObserver keeps measurements in sync and must be disconnected in cleanup.
- Measure in an Effect or event handler, never during render.
- useLayoutEffect fits measurements that must run before the browser paints.
Frequently Asked Questions
Why measure after render instead of during render?
When should I use ResizeObserver instead of a one-time measurement?
Do I need to clean up a ResizeObserver?
Conclusion
Reach the node with a ref, then measure it after React commits the DOM. Use getBoundingClientRect for one snapshot and ResizeObserver, with proper cleanup, to stay in sync as layout changes.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.