useElementSize With ResizeObserver

Measuring a DOM node's width and height in React without listening to `window.resize`. Uses `ResizeObserver` so it fires for layout-driven changes (sidebar toggling, font load, parent flex) too.

JavaScript
Frontend
3 snippets
react
hooks
performance-optimization
resize-observer
lilykelly

By @lilykelly

April 7, 2026

·

Updated May 18, 2026

977 views

30

4.4 (8)

// useElementSize: measure any element via ResizeObserver. The ref is a callback
// ref so it works whether the consumer renders the target conditionally or not.
const { useState, useCallback, useRef, useEffect } = (typeof React !== 'undefined' ? React : {
    useState: (init) => {
        let v = typeof init === 'function' ? init() : init;
        return [v, (n) => { v = typeof n === 'function' ? n(v) : n; return v; }];
    },
    useCallback: (f) => f,
    useRef: (init) => ({ current: init }),
    useEffect: () => {},
});

function useElementSize() {
    const [size, setSize] = useState({ width: 0, height: 0 });
    const observerRef = useRef(null);
    const elementRef = useRef(null);

    const setRef = useCallback((node) => {
        if (observerRef.current) {
            observerRef.current.disconnect();
            observerRef.current = null;
        }
        elementRef.current = node;
        if (!node || typeof ResizeObserver === 'undefined') return;
        const obs = new ResizeObserver((entries) => {
            const entry = entries[0];
            if (!entry) return;
            const { width, height } = entry.contentRect;
            setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));
        });
        obs.observe(node);
        observerRef.current = obs;
    }, []);

    useEffect(() => () => {
        if (observerRef.current) observerRef.current.disconnect();
    }, []);

    return [setRef, size];
}

// Demo: simulate a node with getBoundingClientRect, drive setRef and a ResizeObserver entry.
const [setRef, size] = useElementSize();
const fakeNode = { tagName: 'DIV' };
setRef(fakeNode);
console.log('initial size:', size);
console.log('setRef returns void:', setRef(null) === undefined);
console.log('hook surface keys:', Object.keys({ setRef, size }));

I always reach for a callback ref over useRef + useEffect for measurement hooks because the timing is cleaner: the callback runs synchronously when React attaches or detaches the node, so I never measure before mount or leak an observer after unmount. The Object.is check inside setSize keeps the state stable when the browser fires noisy ResizeObserver events with the same dimensions, which I have seen happen during scroll-into-view animations. Storing the observer in a ref lets me disconnect cleanly when the consumer swaps elements (a common pattern in conditional rendering).