All files / hooks/use-measure/src useMeasure.ts

100% Statements 16/16
100% Branches 8/8
100% Functions 5/5
100% Lines 15/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89                                                                                                32x 32x       32x 14x 1x   13x 13x                 32x   3x     32x   12x   12x     12x 11x           32x    
import { useCallback, useRef, useState } from "react";
import { useResizeObserver } from "@usefy/use-resize-observer";
import type { Bounds, UseMeasureRef, UseMeasureReturn } from "./types";
import { EMPTY_BOUNDS, areBoundsEqual, rectToBounds } from "./utils";
 
/**
 * Reactively measure an element's bounds — its size **and** viewport-relative
 * position — and get fresh values whenever the element resizes.
 *
 * `useMeasure` is the ergonomic, "just give me the bounds" convenience layer
 * built on top of {@link useResizeObserver}. Where `useResizeObserver` is the
 * low-level primitive (raw entries, box models, debounce/throttle, callbacks),
 * `useMeasure` reuses it internally purely to know *when* to re-measure, then
 * reads `getBoundingClientRect()` to return the full
 * `{ x, y, width, height, top, right, bottom, left }` bounds object. Reach for
 * `useResizeObserver` when you need the observer's knobs; reach for `useMeasure`
 * when you just want the current rect.
 *
 * Behaviour notes:
 * - **SSR-safe**: returns {@link EMPTY_BOUNDS} on the server and never touches
 *   the DOM until an element is attached on the client.
 * - **StrictMode / concurrent-safe**: the state update is an idempotent,
 *   equality-guarded `setState`; no user callback is fired from inside it.
 * - Bounds come from `getBoundingClientRect()`, so they update on resize (via
 *   `ResizeObserver`) but not on scroll — scroll does not change an element's
 *   size. Re-measure manually (e.g. re-attach the ref) if you need scroll
 *   tracking.
 *
 * @typeParam T - The element type being measured (defaults to `Element`).
 * @returns A `[ref, bounds]` tuple — attach `ref` to the target element and
 *   read the latest {@link Bounds} from `bounds`.
 *
 * @example
 * ```tsx
 * import { useMeasure } from "@usefy/use-measure";
 *
 * function Card() {
 *   const [ref, bounds] = useMeasure<HTMLDivElement>();
 *
 *   return (
 *     <div ref={ref} style={{ resize: "both", overflow: "auto" }}>
 *       {Math.round(bounds.width)} × {Math.round(bounds.height)}
 *     </div>
 *   );
 * }
 * ```
 */
export function useMeasure<T extends Element = Element>(): UseMeasureReturn<T> {
  const [bounds, setBounds] = useState<Bounds>(EMPTY_BOUNDS);
  const elementRef = useRef<T | null>(null);
 
  // Read the element's current rect and commit it, skipping the update when the
  // bounds are unchanged so React can bail out of the re-render.
  const measure = useCallback((element: T | null) => {
    if (!element || typeof element.getBoundingClientRect !== "function") {
      return;
    }
    const next = rectToBounds(element.getBoundingClientRect());
    setBounds((prev) => (areBoundsEqual(prev, next) ? prev : next));
  }, []);
 
  // Reuse useResizeObserver as the low-level observer. We only use it as a
  // "when to re-measure" trigger; the callback is stored in a ref inside the
  // hook, so re-creating this arrow each render never re-registers the observer.
  // `updateState: false` skips the inner hook's own size state machine (its
  // setState + computedValues memo) that useMeasure would otherwise discard —
  // onResize still fires regardless, so the trigger is unaffected.
  const { ref: observeRef } = useResizeObserver<T>({
    updateState: false,
    onResize: () => measure(elementRef.current),
  });
 
  const ref = useCallback<UseMeasureRef<T>>(
    (element) => {
      elementRef.current = element;
      // Forward to the observer so it (un)observes the node.
      observeRef(element);
      // Measure synchronously on attach so bounds are available before the
      // observer's first (asynchronous) callback fires.
      if (element) {
        measure(element);
      }
    },
    [observeRef, measure]
  );
 
  return [ref, bounds];
}