ResizeObserver
Observe element size changes without window.resize — box options, loop limits, disconnect, and chart/layout patterns.
- javascript
- resizeobserver
window.resize only cares about the viewport. Components need to know when their box changed — sidebar collapse, flex reflow, container queries via JS for charts. ResizeObserver fires when observed elements’ size changes.
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
// entry.borderBoxSize / contentBoxSize / devicePixelContentBoxSize (arrays in spec)
chart.resize(width, height);
}
});
ro.observe(container);
// ro.unobserve(container);
// ro.disconnect();
Entry fields
function sizeOf(entry) {
// Prefer box-size APIs when available
const box = entry.borderBoxSize?.[0];
if (box) {
return { width: box.inlineSize, height: box.blockSize };
}
return {
width: entry.contentRect.width,
height: entry.contentRect.height,
};
}
observe(el, { box: 'border-box' }) controls which box is watched (content-box, border-box, device-pixel-content-box).
Why not just offsetWidth in layout effect?
You can measure once after mount, but later size changes (font load, sibling flex, async content) won’t notify you. ResizeObserver covers the ongoing cases without polling.
Observation delivery
Callbacks are delivered asynchronously (like other observers), batched. Don’t assume they run before paint of the frame that changed size — design updates to be tolerant.
Resize loop errors
If your callback changes size of an observed element in a way that feeds back, browsers can report ResizeObserver loop limit exceeded (often a window error event, non-fatal). Patterns:
const ro = new ResizeObserver((entries) => {
const { width } = sizeOf(entries[0]);
// Write to a child, not the observed root, when possible
canvas.width = width * devicePixelRatio;
});
ro.observe(wrapper); // wrapper size driven by layout, not by canvas width alone
Avoid: observe el → set el.style.height based on measurement → infinite churn.
Cleanup
useEffect(() => {
const ro = new ResizeObserver(handler);
ro.observe(ref.current);
return () => ro.disconnect();
}, []);
Leaking observers keeps elements and closures alive — SPA leak classic.
vs container queries / media queries
CSS container queries handle many pure-style cases without JS. Use ResizeObserver when you need imperative reaction (canvas, third-party chart libs, virtualization measurements).
Interview answer (out loud)
“ResizeObserver notifies when an element’s box changes, which window.resize can’t do for internal layout. I read contentRect or borderBoxSize, disconnect on teardown, and avoid feedback loops that resize the observed node from the callback. For pure CSS responses I prefer container queries.”
Debouncing observer spam
Some layouts fire many observations during drag-resize. Debounce the expensive work, not the observer registration:
let raf = 0;
const ro = new ResizeObserver((entries) => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
applySize(sizeOf(entries[0]));
});
});
Coalesce to one update per frame.
Device pixel box
ro.observe(canvas, { box: 'device-pixel-content-box' });
Useful when backing store resolution must match physical pixels for sharp canvas drawing. Feature-detect: older engines may not support that box.
Testing
In unit tests, jsdom historically lacked ResizeObserver — polyfill or mock:
class RO {
observe() {}
unobserve() {}
disconnect() {}
}
global.ResizeObserver = RO;
Integration tests in real browsers catch the feedback-loop class of bugs.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.