kripod/react-hooks

Add hook for measuring components

kripod opened this issue · 5 comments

Motivation

One may need to know the exact size of a DOM node, to know whether it's big or small enough.

Details

How can I measure a DOM node? – React Hooks FAQ:

function MeasureExample() {
  const [rect, ref] = useClientRect();
  return (
    <>
      <h1 ref={ref}>Hello, world</h1>
      {rect !== null &&
        <h2>The above header is {Math.round(rect.height)}px tall</h2>
      }
    </>
  );
}

function useClientRect() {
  const [rect, setRect] = useState(null);
  const ref = useCallback(node => {
    if (node !== null) {
      setRect(node.getBoundingClientRect());
    }
  }, []);
  return [rect, ref];
}
cmoog commented

Seems like you'd need to be able to refresh the measurement, maybe on a scroll event or after some animation etc. What do you think about the following modification?

Simple usage

const [rect, ref, refresh] = useNodeRect()

Implementation

function useNodeRect() {
  const [rect, setRect] = useState(null)
  const ref = useRef()

  const refresh = useCallback(ref => {
    if (ref.current) {
      setRect(ref.current.getBoundingClientRect())
    }
  }, [])

  useEffect(() => {
    refresh(ref)
  }, [ref])

  return [rect, ref, refresh]
}

Personally, I'm thinking about using the ResizeObserver API, but it comes with the cost of polyfilling as of today:

import { useCallback, useMemo, useState, useEffect } from 'react';
import ResizeObserver from 'resize-observer-polyfill';

export default function useSize() {
  const [size, setSize] = useState<Readonly<[number, number]>>([0, 0]);
  const ro = useMemo(
    () =>
      new ResizeObserver(([entry]) => {
        setSize([entry.contentRect.width, entry.contentRect.height]);
      }),
    [],
  );

  const ref = useCallback(
    node => {
      if (node) {
        ro.observe(node);
      }
    },
    [ro],
  );

  useEffect(() => {
    return () => {
      ro.disconnect();
    };
  }, [ro]);

  return [size, ref];
}

Also, I'm not entirely sure about the ro.disconnect() part.

cmoog commented

Looks like that's what react-use has as well.

Here is an improved snippet of code, which makes sure not to observe the same target multiple times:

import { useCallback, useMemo, useState } from 'react';
import ResizeObserver from 'resize-observer-polyfill';

export default function useSize(): [
  Readonly<[number, number]>,
  (element: HTMLElement) => void,
] {
  const [size, setSize] = useState<Readonly<[number, number]>>([0, 0]);

  const ro = useMemo(
    () =>
      new ResizeObserver(([entry]) => {
        setSize([entry.contentRect.width, entry.contentRect.height]);
      }),
    [],
  );

  const ref = useCallback(
    (element: HTMLElement) => {
      // Avoid observing the same target multiple times
      ro.disconnect();

      if (element) {
        ro.observe(element);
      }
    },
    [ro],
  );

  return [size, ref];
}

This has been resolved by bb74f0b.