Search code examples
reactjstypescriptreact-hooksreact-ref

How to create a vector of references and pass it to children components?


I have a parent component which uses N children components, where N depends on the props in input. For example:

function Parent({ cards }: { cards: Cards[] }) {
  return (
    <>
      {cards.map((value, index) => 
        <ChildComponent ref={TODO} value={value} key={value.id} />
      }
    </>
}

Since I have a bunch of callbacks in the components (not shown) that need to know the location on the screen of each child, I need to create a bunch of refs in the parent component and pass them to the children, so that later I can use refs[n].current.getBoundingClientRect() in one of my callbacks.

All the refs need to be recalculated whenever cards change (cards is the state of an GrandParent component, modified as usual with setState(newCards)).

I already tried many things, but nothing really seems to work.

const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
if (cardsRefs.current.length !== cards.length) {
  cardsRefs.current = Array(cards.length)
    .fill(null)
    .map((_, i) => createRef());
}

...

<ChildComponent ref={cardsRefs.current[index]} value={value} key={value.id} />

This sort of works, but I think it's incorrect - if the cards array changes (keeping the same length), I do need to have new refs, right? Also the use of createRef sounds weird.

Another try:

const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
...
<ChildComponent
  ref={(el) => cardsRef.current[index] = el}
  value={value}
  key={value.id}
/>

I have no idea how this is meant to work, or why. In any case it does not for me. I get variations of the same error message:

Type 'HTMLDivElement | null' is not assignable to type 'RefObject'. Type 'null' is not assignable to type 'RefObject'

This happens with useRef<React.RefObject<HTMLDivElement>[]>([]), useRef<HTMLDivElement[]>([]), useRef([]) - where the error complains about different types every time. Not sure what type this is meant to be.

Another try:

const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);

useEffect(() => {
  cardsRefs.current = Array(cards.length)
    .fill(null)
    .map((_, i) => createRef<HTMLDivElement>());
}, [cards]);

This does not work at all, I suspect because useEffect runs after the DOM has been constructed and therefore the ref to the child components have already been passed (and are undefined).

const cardsRefs = useMemo(
  () => useRef<React.RefObject<HTMLDivElement>[]>([]),
  [cards]
)

This does not work because I cannot call useRef inside useMemo.

Ok so basically I don't know what's the right solution here.


Solution

  • Your approaches are close but require minor tweaks.

    Approach 1 - Precomputing the HTMLDivElement Refs

    You are overcomplicating things a bit by trying to use the useEffect or useMemo hooks. You can simply compute the refs as needed, using the createRef utility for the refs you are storing in the cardsRefs.

    In other words, cardsRefs is a React ref that stores an array of React.RefObject<HTMLDivElement> refs.

    Refs are mutable buckets, so map the cards array to previously created refs, or create new refs as necessary.

    const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
    
    cardsRefs.current = cards.map((_, i) => cardsRefs.current[i] || createRef());
    

    Because you are storing an array of React refs, you not only need to access the current value of cardsRefs, but also that of any array element ref.

    cardsRefs.current[n].current?.getBoundingClientRect();
              ^^^^^^^    ^^^^^^^
    

    Parent

    function Parent({ cards }: { cards: Cards[] }) {
      const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
      cardsRefs.current = cards.map((_, i) => cardsRefs.current[i] || createRef());
    
      const someCallback = () => {
        cardsRefs.current[n].current?.getBoundingClientRect();
      };
    
      return (
        <>
          {cards.map((value, index) => (
            <ChildComponent
              ref={cardsRefs.current[index]}
              value={value}
              key={value.id}
            />
          ))}
        </>
      );
    }
    

    Approach 2 - Using the Legacy Ref Callback Syntax

    When using the legacy callback syntax the Refs can potentially be null, which is what the error informs you of.

    Type 'HTMLDivElement | null' is not assignable to type 'RefObject'. Type 'null' is not assignable to type 'RefObject'

    It tells you exactly what the cardsRef type needs to be, e.g. an array of HTMLDivElement | null.

    const cardsRefs = useRef<(HTMLDivElement | null)[]>([]);
    

    The difference here is that you are not storing an array of React refs, but instead an array of references to an HTML div element, or null, so the callback access will be a bit different.

    cardsRefs.current[n]?.getBoundingClientRect();
              ^^^^^^^
    

    Parent

    function Parent({ cards }: { cards: Cards[] }) {
      const cardsRefs = useRef<(HTMLDivElement | null)[]>([]);
    
      const someCallback = () => {
        cardsRefs.current[n]?.getBoundingClientRect();
      };
    
      return (
        <>
          {cards.map((value, index) => (
            <ChildComponent
              ref={(el) => (cardsRefs.current[index] = el)}
              value={value}
              key={value.id}
            />
          ))}
        </>
      );
    }