Neil
Neil

Reputation: 656

Track dimensions of an array of refs

I have a component that returns an array of refs when mapping the {children}

import { useRef } from "react";

const children = ["hey", "now", "heeeey", "now"];

const Test = () => {
  const itemRefs = useRef([]);

  const setItemRef = (element, index) => {
    itemRefs.current[index] = element;
  };

  console.log(itemRefs)
  //current: Array(4)
  //0: <div>hey</div>
  //1: <div>now</div>
  //2: <div>heeeey</div>
  //3: <div>now</div>

  return (
    <>
      {children.map((child, index) => {
        return (
          <div ref={(element) => setItemRef(element, index)} key={index}>
            {child}
          </div>
        );
      })}
    </>
  );
};

export default Test;

I have a hook that tracks the dimensions of a supplied ref.

export default function useBoundingRect(ref, limit) {
  const [boundingClient, setBoundingClient] = useState({
    x: 0,
    y: 0,
    width: 0,
    height: 0
  });

  useEffect(() => {
    if (typeof window !== "undefined" && ref.current) {
      const set = () => {
        const { x, y, width, height } = ref.current.getBoundingClientRect();
        setBoundingClient({ x, y, width, height });
      };

      set();

      const listener = debounce(limit ? limit : 100, set);

      window.addEventListener("resize", listener);
      return () => window.removeEventListener("resize", listener);
    }
  }, [ref, limit]);

  return boundingClient;
}

How can I track the items in itemRefs?

Do I pass the entire object into the hook and modify the hook to return an array of dimensions?

Upvotes: 0

Views: 511

Answers (1)

Linda Paiste
Linda Paiste

Reputation: 42218

One of the fundamental rules of refs is that you must call the same amount of refs with each render. This means that we can't call useBoundingRect in a loop because the length of children might change.

To get around this, we can create an element ChildWrapper that goes around each child. This element calls the hook and communicates with the parent.

const ChildWrapper = ({children, setRect}) => {
  const ref = useRef(null);

  const rect = useBoundingRect(ref);

  useEffect(() => {
    setRect(rect);
  }, [rect]);

  return <div ref={ref}>{children}</div>;
};

const Test = () => {
  // an array of rectangle dimensions
  const [rects, setRects] = useState([]);

  // helper to update a rect by index
  const setRect = (index) => (rect) => {
    // I am using slice instead of map to support changes in array length
    setRects((prev) => [
      ...prev.slice(0, index),
      rect,
      ...prev.slice(index + 1)
    ]);
  };

  console.log("rects", rects);

  return (
    <>
      {children.map((child, index) => {
        return (
          <ChildWrapper key={index} setRect={setRect(index)}>
            {child}
          </ChildWrapper>
        );
      })}
    </>
  );
};

Upvotes: 1

Related Questions