262

As far as I understood I can use refs for a single element like this:

const { useRef, useState, useEffect } = React;

const App = () => {
  const elRef = useRef();
  const [elWidth, setElWidth] = useState();

  useEffect(() => {
    setElWidth(elRef.current.offsetWidth);
  }, []);

  return (
    <div>
      <div ref={elRef} style={{ width: "100px" }}>
        Width is: {elWidth}
      </div>
    </div>
  );
};

ReactDOM.render(
  <App />,
  document.getElementById("root")
);
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<div id="root"></div>

How can I implement this for an array of elements? Obviously not like that: (I knew it even I did not try it:)

const { useRef, useState, useEffect } = React;

const App = () => {
  const elRef = useRef();
  const [elWidth, setElWidth] = useState();

  useEffect(() => {
    setElWidth(elRef.current.offsetWidth);
  }, []);

  return (
    <div>
      {[1, 2, 3].map(el => (
        <div ref={elRef} style={{ width: `${el * 100}px` }}>
          Width is: {elWidth}
        </div>
      ))}
    </div>
  );
};

ReactDOM.render(
  <App />,
  document.getElementById("root")
);
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<div id="root"></div>

I have seen this and hence this. But, I'm still confused about how to implement that suggestion for this simple case.

devserkan
  • 16,870
  • 4
  • 31
  • 47
  • Forgive me if this is ignorant, but if you’re only calling `useRef()` once, why do you expect the elements to have different refs? AFAIK, React uses the ref as an identifier for iterated elements, so it doesn’t know the difference between them when you use the same ref – MTCoster Feb 11 '19 at 15:21
  • 10
    No ignorance here since I'm still learning hooks and refs. So any advice is good advice for me. This is what I want to do, dynamically create different refs for different elements. My second example is just "Do not use this" example :) – devserkan Feb 11 '19 at 15:26
  • Where did [1,2,3] come from? Is it static? The answer depends on it. – Estus Flask Feb 11 '19 at 15:29
  • Eventually, they will come from a remote endpoint. But for now, if I learn the static one I will be glad. If you can explain for the remote situation that would be awesome. Thanks. – devserkan Feb 11 '19 at 15:31
  • How to achieve dynamic ref in recursive component rendering ? – Kanish Nov 13 '22 at 04:24
  • @EstusFlask questions on SO often have simplified examples, and one such simplification is to use a static array like `[1,2,3]`. However, you must never assume that the real-world usage is as simple, and should err on the side of caution that the input data could be much more complex than the simplified example. For example, the array could be dynamic with varying length, it could be coming from a server as JSON, it could have different types of elements, etc. You can write answers that mirror the simple example, but your solutions must be reasonably scalable to more complex input. – ADTC Sep 01 '23 at 05:46

19 Answers19

327

As you cannot use hooks inside loops, here is a solution in order to make it work when the array changes over the time.

I suppose the array comes from the props :

const App = props => {
    const itemsRef = useRef([]);
    // you can access the elements with itemsRef.current[n]

    useEffect(() => {
       itemsRef.current = itemsRef.current.slice(0, props.items.length);
    }, [props.items]);

    return props.items.map((item, i) => (
      <div 
          key={i} 
          ref={el => itemsRef.current[i] = el} 
          style={{ width: `${(i + 1) * 100}px` }}>
        ...
      </div>
    ));
}
Olivier Boissé
  • 15,834
  • 6
  • 38
  • 56
  • refs for an array of items whose size is not known beforehand. – MeanMan Jul 26 '19 at 11:57
  • this solution should work when the size is not known beforehand, `useEffect` is executed after the render, it resizes the array to fit actual number of items – Olivier Boissé Jul 26 '19 at 13:35
  • 72
    Excellent! An extra note, in TypeScript the signature of `itemsRef` appears to be: ```const itemsRef = useRef>([])``` – Mike Niebling Aug 30 '19 at 21:57
  • What about class-based components? Since hooks don't work inside class based comps... – Yuri Pereira Nov 13 '19 at 14:51
  • 2
    you can get the same result in a class component by creating an instance variable in the constructor with `this.itemsRef = []`. Then you need to move the useEffect code inside the componentDidUpdate lifecycle method. Finally in the `render` method you should use `
    `this.itemsRef.current[i] = el} ` to store the refs
    – Olivier Boissé Nov 13 '19 at 16:17
  • 8
    This is not wokring for me. – Vinay Prajapati Dec 03 '19 at 10:08
  • 1
    How does this work if the array expected may be bigger? – madav Dec 23 '19 at 11:18
  • From callback refs to `React.createRef()` and back again to play nice with hooks. – Drazen Bjelovuk Feb 25 '20 at 22:15
  • Could it be a good idea to use `props.items.length` in the `useEffect` array? – Jamneck Jun 13 '20 at 12:09
  • @LuckyStrike yes of course since the size of the array would be the same, it's not necessary to call the `slice` method, but the benefit would be very very small – Olivier Boissé Jun 13 '20 at 16:24
  • 1
    this does not work for me I get undefined as the ref prof on the element – Hugo Sep 22 '20 at 09:40
  • This was a nice solution for me, thanks! I passed the ref through to a component with forwardRef and then used the `ref={el => itemsRef.current[i] = el}` inside the map of its render – Kitson Oct 13 '20 at 11:24
  • How to properly write TypeScript signature for `const itemsRef = useRef([]); ` if I map my array of items to array of `const DocumentEntry = React.forwardRef<{ setEditable(editable: boolean): void; }, EntryProps>(DefaultDocumentEntry);` ? I need to get `setEditable` method on every instance of component in array of instances... I tried `const entriesInstances = React.useRef([]); ` and `const entriesInstances = React.useRef([]);` - but both typings give me errors. – Marecky Nov 19 '20 at 13:48
  • You can actually use hooks inside of loops. Sometimes it is useful. You just have to ensure the loop runs the same number of times each render. – ICW Sep 27 '21 at 14:53
  • I am new to react / js, so sorry for my naivety, but ref attribute has a callback function? Also, how would one go about knowing such information without using stackoverflow? Is there a documentation / handbook that I can use? Thanks – constantlyFlagged Feb 12 '22 at 23:43
  • 1
    On the react documentation, there is a section [callback-refs](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) – Olivier Boissé Feb 13 '22 at 22:31
  • for the typescript users, e.g. @Marecky in this case the type would be `const itemsRef =useRef([]);` – Kevin K. Feb 25 '22 at 13:00
  • 3
    Anyone minds to explain what the reason behind using `slice` is instead of just simply resetting the ref to an empty array – Advena Apr 06 '22 at 06:59
  • 2
    @Tanckom Because `useEffect` is called after the component rendering (so after the ref callback are called). If you reset `itemsRef` to an empty array inside `useEffect`, then you would lose all your elements. – Olivier Boissé Sep 07 '22 at 15:25
  • @OlivierBoissé I'm only following partially. If `props.items = ["a","b","c","d"]`, then why use slice here, which already does a shallow copy? Why not spread syntax? `itemsRef.current = {...itemsRef.current}`. Additionally, I understand that `useEffect` runs after the DOM has been rendered, but I don't get why there's a need to run the `slice` method inside the `useEffect`? – Advena Sep 12 '22 at 10:40
  • slice is used to resize the array (if the new items array is smaller than the previous one, slice will return an array which size will be equal to the new items array size) – Olivier Boissé Sep 13 '22 at 13:02
  • it wont work when the items getting bigger than original items array – jason Jun 21 '23 at 16:30
158

A ref is initially just { current: null } object. useRef keeps the reference to this object between component renders. current value is primarily intended for component refs but can hold anything.

There should be an array of refs at some point. In case the array length may vary between renders, an array should scale accordingly:

const arrLength = arr.length;
const [elRefs, setElRefs] = React.useState([]);

React.useEffect(() => {
  // add or remove refs
  setElRefs((elRefs) =>
    Array(arrLength)
      .fill()
      .map((_, i) => elRefs[i] || createRef()),
  );
}, [arrLength]);

return (
  <div>
    {arr.map((el, i) => (
      <div ref={elRefs[i]} style={...}>
        ...
      </div>
    ))}
  </div>
);

This piece of code can be optimized by unwrapping useEffect and replacing useState with useRef but it should be noted that doing side effects in render function is generally considered a bad practice:

const arrLength = arr.length;
const elRefs = React.useRef([]);

if (elRefs.current.length !== arrLength) {
  // add or remove refs
  elRefs.current = Array(arrLength)
    .fill()
    .map((_, i) => elRefs.current[i] || createRef());
}

return (
  <div>
    {arr.map((el, i) => (
      <div ref={elRefs.current[i]} style={...}>
        ...
      </div>
    ))}
  </div>
);
Mario Petrovic
  • 7,500
  • 14
  • 42
  • 62
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 1
    Thank you for your answer @estus. This clearly shows how I can create refs. Can you provide a way how can I use these refs with "state" if possible, please? Since at this state I can't use any of refs if I'm not wrong. They are not created before the first render and somehow I need to use `useEffect` and state I guess. Let's say, I want to get those elements' widths using refs as l did in my first example. – devserkan Feb 11 '19 at 18:12
  • I'm not sure if I understood you correctly. But a state likely needs to be an array as well, something like `setElWidth(elRef.current.map(innerElRef => innerElRef.current.offsetWidth)]` – Estus Flask Feb 11 '19 at 18:23
  • Oh, thank you. Firstly, I thought to keep the state as an object and make a relation between elements but since elements' and refs' length will be the same I think I can use this logic. – devserkan Feb 11 '19 at 21:21
  • 7
    it only works if the array is always of the same length, if the length varies, your solution will not work. – Olivier Boissé May 09 '19 at 15:31
  • where would you execute this instruction `elRef.current[i] = elRef.current[i] || createRef()` ? – Olivier Boissé May 09 '19 at 15:44
  • 1
    @OlivierBoissé In the code above this would happen inside `.map((el, i) => ...`. – Estus Flask May 09 '19 at 15:49
  • Clever solution. Excellent example of the significance of `createRef` signature. – codeVerine Jul 04 '19 at 11:13
  • You're missing a brace. The first line should be: `const elRef = useRef([...Array(3)].map(() => createRef()));` – robsco Jul 10 '19 at 14:02
  • @EstusFlask `.map((el, i) =>` only gets called when the component is first initialized. If the array length (3 in this case) changed based on a prop, the new elements wouldn't get initialized. – Greg Feb 19 '20 at 18:45
  • @Greg This is what OP asked about, this was noted in the post, *In case array length is fixed*. FWIW, I updated the code to be more flexible. – Estus Flask Feb 19 '20 at 21:07
  • @EstusFlask useEffect would be called after the render method, so those refs would be created after they're used – Greg Feb 19 '20 at 21:46
  • @Greg That's correct. This is not a problem because elements will be rerendered after `elRefs` update with correct refs. It's possible to optimize the comp by eliminating state updates and useEffect (I guess that's what you suggest in your answer) but I cannot recommend this in over-generalized solution like this one. – Estus Flask Feb 19 '20 at 21:56
  • @EstusFlask not following what the upside is to your solution at the cost doing an additional render – Greg Feb 19 '20 at 22:17
  • 1
    @Greg The upside is to not have side effects in render function, which is considered a bad practice that is acceptable but shouldn't be recommended as a rule of thumb. If I did it the opposite way for the sake of preliminary optimization, it would be a reason to criticize the answer, too. I cannot think of a case that would make in-place side effect a really bad choice here but this doesn't mean it doesn't exist. I'll just leave all the options. – Estus Flask Feb 20 '20 at 10:29
  • @Hugo The answer contains workable code. It may not work for you if you don't use in a way it doesn't work. The problem is likely specific to your case, consider posting a question that reflects it. – Estus Flask Sep 22 '20 at 09:49
  • @EstusFlask, could you kindly explain to me how could I pass ```ref``` into a ```hook```? I would like to adapt your solution into a custom ```hook``` but I am quite lost in how could I pass ```ref={elRef[i]}``` as an argument connecting it into a ```hook```. Thanks =) – Arp Sep 29 '20 at 01:21
  • @Alioshr If I understood your case correctly, you don't need to pass it to a hook. `useRef` needs to be called inside a hook. A hook that is similar to described case would contain the code from the last snippet as is, up to `return
    `, `let myHook = arrLength => { ...; return elRefs }`. And would be used in the component as `let elRefs=myHook(arr.length)`.
    – Estus Flask Oct 01 '20 at 19:49
  • Where I'm admittedly confused is why `useEffect(() => { // array fill code }, [arr.length])` can't be used to avoid your render side effect and instead nets refs in the array that are always null... I suspect because `createRef()` is closed by the effect closure? – spacesuitdiver Apr 21 '22 at 06:53
  • @ChrisLeBlanc Can you clarify what you mean? Is it just arr.length vs arrLength, or "array fill code" is supposed to be something different? – Estus Flask Apr 21 '22 at 09:26
  • 1
    You can omit that .fill(), just make sure you initiate the state with createRef: `React.useState>([createRef()])` – Code Drop Jan 22 '23 at 20:06
83

Update

New React Doc shows a recommended way by using map.

Check the Beta version here (Dec, 2022)


There are two ways

  1. use one ref with multiple current elements
const inputRef = useRef([]);

inputRef.current[idx].focus();

<input
  ref={el => inputRef.current[idx] = el}
/>

const {useRef} = React;
const App = () => {
  const list = [...Array(8).keys()];
  const inputRef = useRef([]);
  const handler = idx => e => {
    const next = inputRef.current[idx + 1];
    if (next) {
      next.focus()
    }
  };
  return (
    <div className="App">
      <div className="input_boxes">
        {list.map(x => (
        <div>
          <input
            key={x}
            ref={el => inputRef.current[x] = el} 
            onChange={handler(x)}
            type="number"
            className="otp_box"
          />
        </div>
        ))}
      </div>
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
  1. use an Array of ref

    As the above post said, it's not recommended since the official guideline (and the inner lint check) won't allow it to pass.

    Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders.

    However, since it's not our current case, the demo below still works, only not recommended.

const inputRef = list.map(x => useRef(null));

inputRef[idx].current.focus();

<input
  ref={inputRef[idx]}
/>

const {useRef} = React;
const App = () => {
const list = [...Array(8).keys()];
const inputRef = list.map(x => useRef(null));
const handler = idx => () => {
  const next = inputRef[idx + 1];
  if (next) {
    next.current.focus();
  }
};
return (
  <div className="App">
    <div className="input_boxes">
      {list.map(x => (
      <div>
        <input
          key={x}
          ref={inputRef[x]}
          onChange={handler(x)}
          type="number"
          className="otp_box"
        />
      </div>
      ))}
    </div>
  </div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
keikai
  • 14,085
  • 9
  • 49
  • 68
  • option two is what worked for me trying to use `showCallout()` on react-native-maps `Marker`s – 7ahid Jul 06 '20 at 01:25
  • 3
    simple but helpful – Faris Rayhan Oct 02 '20 at 13:05
  • Option #2 is not correct. You may only use hooks only at the top level: https://pl.reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level #2 works for you as long as the length of list is constant, but when you will add a new item to the list, it will throw an error. – Adrian Mar 30 '21 at 06:39
  • @Adrian As I said in the answer, it's not allowed to write in that way, it's not recommended as well, you can choose not to use it and click downvote, but it doesn't let the demo above not works (you can try it as well by click `show code snippet` then `Run`). The reason I still keep the #2 is to make it more clear why there exists the issue. – keikai Mar 30 '21 at 07:11
  • first method works like charm. – zonay Nov 10 '21 at 14:35
  • 1
    Option #1 will unnecessarily re-render all children by returning a new ref function on every render. – Daniel Jan 03 '22 at 22:57
14

All other options above are relying on Arrays but it makes things extremely fragile, as elements might be reordered and then we don't keep track of what ref belongs to what element.

React uses the key prop to keep track of items. Therefore if you store your refs by keys there won't be any problem :

const useRefs = () => {
  const refsByKey = useRef<Record<string,HTMLElement | null>>({})

  const setRef = (element: HTMLElement | null, key: string) => {
    refsByKey.current[key] = element;
  }

  return {refsByKey: refsByKey.current, setRef};
}
const Comp = ({ items }) => {
  const {refsByKey, setRef} = useRefs()

  const refs = Object.values(refsByKey).filter(Boolean) // your array of refs here

  return (
    <div>
      {items.map(item => (
        <div key={item.id} ref={element => setRef(element, item.id)}/>
      )}
    </div>
  )
}

Note that React, when unmounting an item, will call the provided function with null, which will set the matching key entry to null in the object, so everything will be up-to-date.

  • 1
    This is good, works for when you sort your list, others use index which I believe....it no bueno when sorting. as the index and ref will need to be calculated each time. tyty. – RJA Mar 10 '23 at 22:14
  • This is also a nice idea. If not for the answer that says to just use the parent ref and get children as an array, I would have done this. – ADTC Sep 01 '23 at 05:37
12

The simplest and most effective way is to not use useRef at all. Just use a callback ref that creates a new array of refs on every render.

function useArrayRef() {
  const refs = []
  return [refs, el => el && refs.push(el)]
}

Demo

<div id="root"></div>

<script type="text/babel" defer>
const { useEffect, useState } = React

function useArrayRef() {
  const refs = []
  return [refs, el => el && refs.push(el)]
}

const App = () => {
  const [elements, ref] = useArrayRef()
  const [third, setThird] = useState(false)
  
  useEffect(() => {
    console.log(elements)
  }, [third])

  return (
    <div>
      <div ref={ref}>
        <button ref={ref} onClick={() => setThird(!third)}>toggle third div</button>
      </div>
      <div ref={ref}>another div</div>
      { third && <div ref={ref}>third div</div>}
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
</script>

<script src="https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
Joel Davey
  • 2,363
  • 1
  • 21
  • 20
Mohamed E.
  • 333
  • 4
  • 8
10

Note that you shouldn't use useRef in a loop for a simple reason: the order of used hooks does matter!

The documentation says

Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)

But consider that it obviously applies to dynamic arrays... but if you're using static arrays (you ALWAYS render the same amount of components) don't worry too much about that, be aware of what you're doing and leverage it

NoriSte
  • 3,589
  • 1
  • 19
  • 18
8

You can use an array(or an object) to keep track of all the refs and use a method to add ref to the array.

NOTE: If you are adding and removing refs you would have to empty the array every render cycle.

import React, { useRef } from "react";

const MyComponent = () => {
   // intialize as en empty array
   const refs = useRefs([]); // or an {}
   // Make it empty at every render cycle as we will get the full list of it at the end of the render cycle
   refs.current = []; // or an {}

   // since it is an array we need to method to add the refs
   const addToRefs = el => {
     if (el && !refs.current.includes(el)) {
       refs.current.push(el);
     }
    };
    return (
     <div className="App">
       {[1,2,3,4].map(val => (
         <div key={val} ref={addToRefs}>
           {val}
         </div>
       ))}
     </div>
   );

}

working example https://codesandbox.io/s/serene-hermann-kqpsu

Neo
  • 4,550
  • 6
  • 34
  • 51
8

I use the useRef hook to create panels of data that I want to control independently. First I initialize the useRef to store an array:

import React, { useRef } from "react";

const arr = [1, 2, 3];

const refs = useRef([])

When initializing the array we observe that it actually looks like this:

//refs = {current: []}

Then we apply the map function to create the panels using the div tag which we will be referencing, adds the current element to our refs.current array with one button to review:

arr.map((item, index) => {
  <div key={index} ref={(element) => {refs.current[index] = element}}>
    {item}
    <a
      href="#"
      onClick={(e) => {
        e.preventDefault();
        onClick(index)
      }}
    >
      Review
    </a>
})

Finally a function that receives the index of the pressed button we can control the panel that we want to show

const onClick = (index) => {
  console.log(index)
  console.log(refs.current[index])
}

Finally the complete code would be like this

import React, { useRef } from "react";

const arr = [1, 2, 3];

const refs = useRef([])
//refs = {current: []}

const onClick = (index) => {
  console.log(index)
  console.log(refs.current[index])
}

const MyPage = () => {
   const content = arr.map((item, index) => {
     <div key={index} ref={(element) => {refs.current[index] = element}}>
       {item}
       <a
         href="#"
         onClick={(e) => {
           e.preventDefault();
           onClick(index)
         }}
       >
         Review
       </a>
   })
   return content
}

export default MyPage

It works for me! Hoping that this knowledge will be of use to you.

Jorge Pirela
  • 129
  • 1
  • 6
7

Assuming that your array contains non primitives, you could use a WeakMap as the value of the Ref.

function MyComp(props) {
    const itemsRef = React.useRef(new WeakMap())

    // access an item's ref using itemsRef.get(someItem)

    render (
        <ul>
            {props.items.map(item => (
                <li ref={el => itemsRef.current.set(item, el)}>
                    {item.label}
                </li>
            )}
        </ul>
    )
}
Dan F.
  • 1,353
  • 16
  • 23
  • 1
    Actually, in my real case, my array contains non-primitives but I had to loop over the array. I think it is not possible with WeakMap, but it is a good option indeed if no iteration is needed. Thanks. PS: Ah, there is a [proposal](https://github.com/tc39/proposal-weakrefs#iterable-weakmaps) for that and it is now Stage 3. Good to know :) – devserkan Jun 10 '20 at 11:52
  • I am new to react / js, so sorry for my naivety, but ref attribute has a callback function? Also, how would one go about knowing such information without using stackoverflow? Is there a documentation / handbook that I can use? Thanks – constantlyFlagged Feb 12 '22 at 23:43
3

If I understand correctly, useEffect should only be used for side effects, for this reason I chose instead to use useMemo.

const App = props => {
    const itemsRef = useMemo(() => Array(props.items.length).fill().map(() => createRef()), [props.items]);

    return props.items.map((item, i) => (
        <div 
            key={i} 
            ref={itemsRef[i]} 
            style={{ width: `${(i + 1) * 100}px` }}>
        ...
        </div>
    ));
};

Then if you want to manipulate the items / use side effects you can do something like:

useEffect(() => {
    itemsRef.map(e => e.current).forEach((e, i) => { ... });
}, [itemsRef.length])
Gustavo Maximo
  • 4,426
  • 3
  • 17
  • 26
2

React will re-render an element when its ref changes (referential equality / "triple-equals" check).

Most answers here do not take this into account. Even worse: when the parent renders and re-initializes the ref objects, all children will re-render, even if they are memoized components (React.PureComponent or React.memo)!

The solution below has no unnecessary re-renders, works with dynamic lists and does not even introduce an actual side effect. Accessing an undefined ref is not possible. A ref is initialized upon the first read. After that, it remains referentially stable.

const useGetRef = () => {
  const refs = React.useRef({})
  return React.useCallback(
    (idx) => (refs.current[idx] ??= React.createRef()),
    [refs]
  )
}

const Foo = ({ items }) => {
  const getRef = useGetRef()
  return items.map((item, i) => (
    <div ref={getRef(i)} key={item.id}>
      {/* alternatively, to access refs by id: `getRef(item.id)` */}
      {item.title}
    </div>
  ))
}

Caveat: When items shrinks over time, unused ref objects will not be cleaned up. When React unmounts an element, it will correctly set ref[i].current = null, but the "empty" refs will remain.

Daniel
  • 4,949
  • 4
  • 34
  • 50
2

You can avoid the complexity array refs bring in combination with useEffect by moving the children into a separate component. This has other advantages the main one being readability and making it easier to maintain.

const { useRef, useState, useEffect } = React;

const ListComponent = ({ el }) => {
  const elRef = useRef();
  const [elWidth, setElWidth] = useState();

  useEffect(() => {
    setElWidth(elRef.current.offsetWidth);
  }, []);

  return (
    <div ref={elRef} style={{ width: `${el * 100}px` }}>
      Width is: {elWidth}
    </div>
  );
};

const App = () => {

  return (
    <div>
      {[1, 2, 3].map((el, i) => (
        <ListComponent key={i} el={el} />
      ))}
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));
Joel Davey
  • 2,363
  • 1
  • 21
  • 20
  • Another advantage using this approach if [] was a changing data source then everything would update in the natural React flow – Joel Davey Mar 10 '22 at 12:36
  • Disadvantage is that the refs are only for internal use, and can't be used in the parent component. – ADTC Sep 01 '23 at 05:15
2
import React, { useRef } from "react";

export default function App() {
  const arr = [1, 2, 3];

  const refs = useRef([]);

  return (
    <div className="App">
      {arr.map((item, index) => {
        return (
          <div
            key={index}
            ref={(element) => {
              refs.current[index] = element;
            }}
          >
            {item}
          </div>
        );
      })}
    </div>
  );
}

Credits: https://eliaslog.pw/how-to-add-multiple-refs-to-one-useref-hook/

Rahul Chandra
  • 483
  • 7
  • 15
2

With typescript passing all the eslint warnings

const itemsRef = useRef<HTMLDivElement[]>([]);

data.map((i) => (
  <Item
    key={i}
    ref={(el: HTMLDivElement) => {
      itemsRef.current[i] = el;
      return el;
    }}
  />
))

It <Item /> must be constructed using React.forwardRef

Malte Schulze-Boeing
  • 1,055
  • 10
  • 14
1

You can use a parent element to get a bunch of children elements.

In my case I was trying to get a bunch of inputs inside my form element. So I get the form element and use it to handle with all the inputs.

Something like this:

function Foo() {
    const fields = useRef<HTMLFormElement>(null);

    function handlePopUp(e) {
      e.preventDefault();
    
      Array.from(fields.current.children)
        .forEach((input: HTMLInputElement | HTMLTextAreaElement) => {
          input.value = '';
        });
    }

    return (
    <form onSubmit={(e) => handlePopUp(e)} ref={fields}>

      <input
        placeholder="Nome"
        required
        id="name"
        type="text"
        name="name"
      />
      <input
        placeholder="E-mail"
        required
        id="email"
        type="email"
        name="email"
      />
      <input
        placeholder="Assunto"
        required
        id="subject"
        type="text"
        name="subject"
      />
      <textarea
        cols={120}
        placeholder="Descrição"
        required
        id="description"
        name="description"
      />

      <button type="submit" disabled={state.submitting}>enviar</button>
    </form>  
    );
}
ADTC
  • 8,999
  • 5
  • 68
  • 93
  • **Honestly this is the cleanest and simplest solution.** (Why keep track of children separately, when the parent already does so for us? Just ask the parent!) _Thank you for sharing._ – ADTC Sep 01 '23 at 05:34
0

We can't use state because we need the ref to be available before the render method is called. We can't call useRef an arbitrary number of times, but we can call it once:

Assuming arr is a prop with the array of things:

const refs = useRef([]);
// free any refs that we're not using anymore
refs.current = refs.current.slice(0, arr.length);
// initialize any new refs
for (let step = refs.current.length; step < arr.length; step++) {
    refs.current[step] = createRef();
}
Greg
  • 12,119
  • 5
  • 32
  • 34
  • References should be updated in a side effect, like `useEffect()`. `...avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.` https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables – Daniele Orlando Mar 11 '20 at 16:41
0

We can use an array ref to memoize the ref list:

import { RefObject, useRef } from 'react';

type RefObjects<T> = RefObject<T>[];

function convertLengthToRefs<T>(
  length: number,
  initialValue: T | null,
): RefObjects<T> {
  return Array.from(new Array(length)).map<RefObject<T>>(() => ({
    current: initialValue,
  }));
}

export function useRefs<T>(length: number, initialValue: T | null = null) {
  const refs = useRef<RefObjects<T>>(convertLengthToRefs(length, initialValue));

  return refs.current;
}

It is a demo:

const dataList = [1, 2, 3, 4];

const Component: React.FC = () => {
  const refs = useRefs<HTMLLIElement>(dataList.length, null);

  useEffect(() => {
    refs.forEach((item) => {
      console.log(item.current?.getBoundingClientRect());
    });
  }, []);

  return (
    <ul>
      {dataList.map((item, index) => (
        <li key={item} ref={refs[index]}>
          {item}
        </li>
      ))}
    </ul>
  );
};


LimCong
  • 9
  • 2
0

There are some rules of hooks so we should consider all the things related to react hooks.

To use multiple refs or list of refs we can do like that:

  1. Declare a variable for refs inside functional component using React.useRef([]).
  2. Simple example is given below. It is just the use of multiple refs

const refsElements = React.useRef([]) // this line will create a refs array like this {current:[]} const [items, setItems] = React.useState([1,2,3,4,5])

<div>
  {items.map((element, index) => (
    <div key={index} ref={refItem => refsElements.current[index]=refItem}}
      <p>{item}</p>
    </div>
  ))}
</div>
Rahul singh
  • 491
  • 3
  • 2
-2
import { createRef } from "react";

const MyComponent = () => {
  const arrayOfElements = Array.from({ length: 10 }).map((_, idx) => idx + 1);
  const refs = arrayOfElements.map(() => createRef(null));

  const onCLick = (index) => {
    ref[index]?.current?.click();
  };

  return (
    <div>
      <h1>Defaults Elements</h1>
      {arrayOfElements.map((element, index) => (
        <div key={index} ref={refs[index]}>
          Default Elemnt {element}
        </div>
      ))}

      <h2>Elements Handlers</h2>
      {arrayOfElements.map((_, index) => (
        <button key={index} onClick={() => onCLick(index)}>
          Element {index + 1} Handler
        </button>
      ))}
    </div>
  );
};