hooks/elementSize.jsxjavascript
/*
Observes an element's content-box size via ResizeObserver. Used wherever a
component needs to react to its own real rendered size rather than the CSS
viewport (Stage.jsx sizing the whole app; GlobePage.jsx sizing the WebGL
canvas, which react-globe.gl otherwise defaults to window.innerWidth/
innerHeight and never updates on resize).
*/
import { useCallback, useLayoutEffect, useState } from "react";
/**
* @param {{width: number, height: number}} [initialSize] - Value returned before the
* observer's first callback fires. Defaults to {0,0}; pass a best-guess (e.g.
* window.innerWidth/innerHeight) when the element is expected to roughly fill the
* viewport, to avoid a one-frame flash at size 0.
* @returns {Array} A `[ref, size]` tuple: a callback ref to attach to the observed element,
* and its current `{width, height}` content-box size in CSS px.
*/
export const useElementSize = (initialSize = { width: 0, height: 0 }) => {
// A callback ref (not a plain useRef) so the observer setup below re-runs
// whenever the underlying DOM node actually changes - including from null
// to a real element on a *later* commit than the component's first. A
// plain ref + `useLayoutEffect(..., [])` would only ever check
// `ref.current` once, on mount: if the consumer conditionally renders
// `null` on its first render (e.g. GlobePage, while content is still
// loading) and only mounts the real element once state resolves, that
// one-shot effect sees `null` and never gets a second chance.
const [node, setNode] = useState(null);
const ref = useCallback((el) => setNode(el), []);
const [size, setSize] = useState(initialSize);
useLayoutEffect(() => {
if (!node) return undefined;
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
setSize({ width, height });
});
observer.observe(node);
return () => observer.disconnect();
}, [node]);
return [ref, size];
};