Detect when child ref is changing

Viewed 1079

I have the following problem:

There is child component that accepts a ref passed down from the parent component:

const ChildComp = React.forwardRef((props, ref) => <div ref={ref} />)

The parent component creates an array of refs and assigns a ref in the array to one of the children:


const ParentComp = () => {
 const items = [1,2,3]
 const refs = items.map(() => React.createRef()) 

 React.useEffect(() => {
  console.log(refs);
 }, [refs])

 return <div>{items.map((item, index) => <ChildComp ref={refs[index]>)}</div>
}

However, when I output the state of the refs array in useEffect, it outputs it only once when the parent mounts and at that point the ref.current values are still null, because children are not mounted yet.

I would like to be able to have an array of refs where each ref belongs to a child as illustrated above.

2 Answers

Maybe each ref in the refs array is changing but the array itself didn't change. this is why useEffect didn't run again.

You can try:

React.useEffect(() => {
  console.log(refs);
 }, [refs[0].current])

Here is a working Codesandbox: https://codesandbox.io/s/currying-leaf-tpibl?file=/src/App.tsx

I modified your example a bit:

  • use functional components only, as string refs (from createRef) should not be used with them
  • use callback refs instead of creating them, which will react to changes of the node

The important part is

refProp={(node) => (itemsRef.current[index] = node)},

which will fill the itemsRef array based on the callback ref through the ChildComp.

ChildComp

const ChildComp = ({
  number,
  refProp
}: {
  number: number;
  refProp: (node: HTMLDivElement) => void;
}) => {
  // simulate internal child state for demonstration
  const [childState, setChildState] = useState(0);

  useEffect(() => setChildState(number * 2), [number]);

  return (
    <div ref={refProp}>
      <p>
        State from child no. {number}: {childState}
      </p>
    </div>
  );
};

ParentComp

const ParentComp = () => {
  const [items] = useState([1, 2, 3]);
  // you can access the elements with itemsRef.current[n]
  const itemsRef = useRef<Array<HTMLDivElement | null>>([]);

  useEffect(() => {
    // create initial refs array, will be filled through callback ref
    itemsRef.current = itemsRef.current.slice(0, items.length);
  }, [items]);

  useEffect(() => {
    // this logs the three <div>s of the childs
    console.log(itemsRef);
  }, [itemsRef]);

  return (
    <>
      <p>Hi from parent!</p>
      {items.map((_, index) => (
        <ChildComp
          key={index}
          number={index + 1}
          // will be called by React when they're changing
          refProp={(node) => (itemsRef.current[index] = node)}
        />
      ))}
    </>
  );
};
Related