How can I use multiple refs for an array of elements with hooks?
Asked Answered
E

20

291

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.

Emilia answered 11/2, 2019 at 15:18 Comment(6)
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 refMongo
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 :)Emilia
Where did [1,2,3] come from? Is it static? The answer depends on it.Gurule
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.Emilia
How to achieve dynamic ref in recursive component rendering ?Dustydusza
@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.Carencarena
G
162

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>
);
Gurule answered 11/2, 2019 at 15:32 Comment(20)
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.Emilia
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)]Gurule
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.Emilia
it only works if the array is always of the same length, if the length varies, your solution will not work.Hertz
where would you execute this instruction elRef.current[i] = elRef.current[i] || createRef() ?Hertz
@OlivierBoissé In the code above this would happen inside .map((el, i) => ....Gurule
Clever solution. Excellent example of the significance of createRef signature.Community
You're missing a brace. The first line should be: const elRef = useRef([...Array(3)].map(() => createRef()));Sforza
@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.Spirant
@Spirant 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.Gurule
@EstusFlask useEffect would be called after the render method, so those refs would be created after they're usedSpirant
@Spirant 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.Gurule
@EstusFlask not following what the upside is to your solution at the cost doing an additional renderSpirant
@Spirant 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.Gurule
@Overcasting 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.Gurule
@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 =)Crave
@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 <div>, let myHook = arrLength => { ...; return elRefs }. And would be used in the component as let elRefs=myHook(arr.length).Gurule
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?Terramycin
@ChrisLeBlanc Can you clarify what you mean? Is it just arr.length vs arrLength, or "array fill code" is supposed to be something different?Gurule
You can omit that .fill(), just make sure you initiate the state with createRef: React.useState<RefObject<HTMLDivElement | null[]>>([createRef()])Command
A
363

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>
    ));
}
Allochthonous answered 9/5, 2019 at 15:42 Comment(22)
refs for an array of items whose size is not known beforehand.Encumbrancer
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 itemsHertz
Excellent! An extra note, in TypeScript the signature of itemsRef appears to be: const itemsRef = useRef<Array<HTMLDivElement | null>>([])Illation
What about class-based components? Since hooks don't work inside class based comps...Carrier
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 <div key={i} ref={el => this.itemsRef.current[i] = el} ` to store the refsHertz
This is not wokring for me.Casework
How does this work if the array expected may be bigger?Perchloride
From callback refs to React.createRef() and back again to play nice with hooks. 🙃Roofdeck
Could it be a good idea to use props.items.length in the useEffect array?Gingham
@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 smallHertz
this does not work for me I get undefined as the ref prof on the elementOvercasting
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 renderMaremma
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<React.ReactNode[]>([]); and const entriesInstances = React.useRef<typeof DocumentEntry[]>([]); - but both typings give me errors.Suanne
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.Levison
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? ThanksAcanthous
On the react documentation, there is a section callback-refsHertz
for the typescript users, e.g. @Suanne in this case the type would be const itemsRef =useRef<HTMLDivElement[] | null[]>([]);Whitherward
Anyone minds to explain what the reason behind using slice is instead of just simply resetting the ref to an empty arrayLeveloff
@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.Hertz
@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?Leveloff
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)Hertz
it wont work when the items getting bigger than original items arrayLerner
G
162

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>
);
Gurule answered 11/2, 2019 at 15:32 Comment(20)
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.Emilia
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)]Gurule
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.Emilia
it only works if the array is always of the same length, if the length varies, your solution will not work.Hertz
where would you execute this instruction elRef.current[i] = elRef.current[i] || createRef() ?Hertz
@OlivierBoissé In the code above this would happen inside .map((el, i) => ....Gurule
Clever solution. Excellent example of the significance of createRef signature.Community
You're missing a brace. The first line should be: const elRef = useRef([...Array(3)].map(() => createRef()));Sforza
@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.Spirant
@Spirant 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.Gurule
@EstusFlask useEffect would be called after the render method, so those refs would be created after they're usedSpirant
@Spirant 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.Gurule
@EstusFlask not following what the upside is to your solution at the cost doing an additional renderSpirant
@Spirant 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.Gurule
@Overcasting 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.Gurule
@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 =)Crave
@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 <div>, let myHook = arrLength => { ...; return elRefs }. And would be used in the component as let elRefs=myHook(arr.length).Gurule
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?Terramycin
@ChrisLeBlanc Can you clarify what you mean? Is it just arr.length vs arrLength, or "array fill code" is supposed to be something different?Gurule
You can omit that .fill(), just make sure you initiate the state with createRef: React.useState<RefObject<HTMLDivElement | null[]>>([createRef()])Command
M
93

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>
Melloney answered 1/4, 2020 at 18:8 Comment(6)
option two is what worked for me trying to use showCallout() on react-native-maps MarkersHayden
simple but helpfulWitching
Option #2 is not correct. You may only use hooks only at the top level: pl.reactjs.org/docs/… #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.Staid
@Staid 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.Melloney
first method works like charm.Thyroiditis
Option #1 will unnecessarily re-render all children by returning a new ref function on every render.Logjam
D
18

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.

Dissociable answered 21/9, 2022 at 7:31 Comment(2)
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.Prosthodontics
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.Carencarena
E
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>
Event answered 24/7, 2021 at 10:47 Comment(2)
This is good solution if you do have to have it one componentAran
These are called a 'Callback Refs' - reactjs.org/docs/refs-and-the-dom.html#callback-refsUlises
C
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 😉

Cultivable answered 11/2, 2019 at 16:9 Comment(0)
W
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

Wilburwilburn answered 26/1, 2020 at 3:7 Comment(3)
Why, if you're already checking if el is in the array, should you empty it at every render cycle?Sarre
Because every render cycle it will add it to the array, we want only one copy of the el.Wilburwilburn
Yes, but aren't you checking with !refs.current.includes(el)?Sarre
I
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.

Indispensable answered 27/1, 2022 at 20:46 Comment(0)
S
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>
    )
}
Socher answered 10/6, 2020 at 8:21 Comment(2)
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 for that and it is now Stage 3. Good to know :)Emilia
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? ThanksAcanthous
B
4

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

Burgle answered 25/4, 2023 at 21:56 Comment(0)
Y
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])
Yevetteyew answered 18/10, 2020 at 1:28 Comment(0)
L
3

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.

Logjam answered 3/1, 2022 at 22:53 Comment(1)
I like this solution, it works and can easily be plugged in.Logue
A
3

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>  
    );
}
Aestival answered 5/6, 2022 at 18:41 Comment(1)
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.Carencarena
A
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'));
Aran answered 10/3, 2022 at 12:12 Comment(2)
Another advantage using this approach if [] was a changing data source then everything would update in the natural React flowAran
Disadvantage is that the refs are only for internal use, and can't be used in the parent component.Carencarena
D
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/

Donnetta answered 3/6, 2022 at 12:55 Comment(0)
A
1

Despite the following answers are solving the issue:

There are still two suboptimal aspects:

  1. In all these examples and solutions, the ref function is dynamic - it's a new function on every render. This means that on each render cycle, references are detached and reattached for every item (see example here). While this may not be a major concern, it's a practice I personally prefer to avoid.
  2. If there are multiple places in the application where an array of references needs to be handled, it requires repeating the same boilerplate code for initializing the collection of references, handling refs for each item separately etc.

Solution

To address both of these issues, I've developed a library which I've successfully used for several years: react-refs-collection.

This library provides a consistent ref handler function for each item and offers a minimalistic API for working with a collection of refs:

const {getRefHandler, getRef} = useRefsCollection();

const focusItem = useCallback(inputId => {
    getRef(inputId).focus();
}, []);

return (
     <div>
         {inputs.map((input) => (
             <input
                 ref={getRefHandler(input.id)}
                 // ...restProps
             />
         ))}
    </div>
)
Altruist answered 6/4 at 12:34 Comment(1)
That looks neat! I was about to re-invent the already existing wheel. Thanks!Boughpot
S
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();
}
Spirant answered 19/2, 2020 at 21:51 Comment(1)
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. reactjs.org/docs/…Adaminah
G
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>
  );
};


Gloucestershire answered 8/10, 2022 at 8:3 Comment(0)
R
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>
Ribaldry answered 17/8, 2023 at 13:42 Comment(0)
B
-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>
  );
};

Bidden answered 30/1, 2023 at 12:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.