Search code examples
javascriptreactjsreact-hooksuse-ref

Generate multiple refs in map loop


I am still confused if I use useRef([]); the right way, as itemsRef returns Object {current: Array[0]}. Here in action: https://codesandbox.io/s/zealous-platform-95qim?file=/src/App.js:0-1157

import React, { useRef } from "react";
import "./styles.css";

export default function App() {
  const items = [
    {
      id: "asdf2",
      city: "Berlin",
      condition: [
        {
          id: "AF8Qgpj",
          weather: "Sun",
          activity: "Outside"
        }
      ]
    },
    {
      id: "zfsfj",
      city: "London",
      condition: [
        {
          id: "zR8Qgpj",
          weather: "Rain",
          activity: "Inside"
        }
      ]
    }
  ];

  const itemsRef = useRef([]);

  // Object {current: Array[0]}
  // Why? Isn't it supposed to be filled with my refs (condition.id)
  console.log(itemsRef);

  return (
    <>
      {items.map(cities => (
        <div key={cities.id}>
          <b>{cities.city}</b>
          <br />
          {cities.condition.map(condition => (
            <div
              key={condition.id}
              ref={el => (itemsRef.current[condition.id] = el)}
            >
              Weather: {condition.weather}
              <br />
              Activity: {condition.activity}
            </div>
          ))}
          <br />
          <br />
        </div>
      ))}
    </>
  );
}

In the original example I receive // Object {current: Array[3]} when I console.log(itemsRef); The difference is that I used in my version itemsRef.current[condition.id] as its a nested map loop and therefore i doesn't work.

import React, { useRef } from "react";
import "./styles.css";

export default function App() {
  const items = ["sun", "flower", "house"];
  const itemsRef = useRef([]);

  // Object {current: Array[3]}
  console.log(itemsRef);

  return items.map((item, i) => (
    <div key={i} ref={el => (itemsRef.current[i] = el)}>
      {item}
    </div>
  ));
}

Solution

  • You're using non-numeric string keys when adding the refs to itemRefs, which means they end up being properties of the array object, but not array elements, so its length remains 0. Depending on your console, it may or may not show non-element properties on an array object.

    You could make them array elements instead by using the index from map (but keep reading!):

    {cities.condition.map((condition, index) => (
        <div
            key={condition.id}
            ref={el => (itemsRef.current[index] = el)}
        >
            Weather: {condition.weather}
            <br />
            Activity: {condition.activity}
        </div>
    ))}
    

    but depending on what you're doing with those refs I would avoid that in favor of making each condition its own component instead:

    const Condition = ({weather, activity}) => {
        const itemRef = useRef(null);
      
        return (
            <div
                ref={itemRef}
            >
                Weather: {weather}
                <br />
                Activity: {activity}
            </div>
        );
    };
    

    Then get rid of itemRefs and do:

    {cities.condition.map(({id, weather, activity}) => (
        <Condition key={id} weather={weather} activity={activity} />
    ))}
    

    One problem with your current way even if we use array elements is that itemRefs will continue to have three elements in it even when the DOM elements that they used to refer to are gone (they'll have null instead), since React calls your ref callback with null when the element is removed, and your code is just storing that null in the array.

    Alternatively, you might use an object:

    const itemRefs = useRef({});
    // ...
    {cities.condition.map(condition => (
        <div
            key={condition.id}
            ref={el => {
                if (el) {
                    itemsRef.current[condition.id] = el;
                } else {
                    delete itemsRef.current[condition.id];
                }
            }}
        >
            Weather: {condition.weather}
            <br />
            Activity: {condition.activity}
        </div>
    ))}
    

    Or perhaps a Map:

    const itemRefs = useRef(new Map());
    // ...
    {cities.condition.map(condition => (
        <div
            key={condition.id}
            ref={el => {
                if (el) {
                    itemsRef.current.set(condition.id, el);
                } else {
                    itemsRef.current.delete(condition.id);
                }
            }}
        >
            Weather: {condition.weather}
            <br />
            Activity: {condition.activity}
        </div>
    ))}
    

    But again, I'd lean toward making a Condition component that manages its own ref.