328

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.

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 ref Commented Feb 11, 2019 at 15:21
  • 12
    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 :) Commented Feb 11, 2019 at 15:26
  • Where did [1,2,3] come from? Is it static? The answer depends on it. Commented Feb 11, 2019 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. Commented Feb 11, 2019 at 15:31
  • How to achieve dynamic ref in recursive component rendering ? Commented Nov 13, 2022 at 4:24

23 Answers 23

406

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>
    ));
}
Sign up to request clarification or add additional context in comments.

22 Comments

Excellent! An extra note, in TypeScript the signature of itemsRef appears to be: const itemsRef = useRef<Array<HTMLDivElement | null>>([])
This is not wokring for me.
Anyone minds to explain what the reason behind using slice is instead of just simply resetting the ref to an empty array
@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.
it wont work when the items getting bigger than original items array
|
174

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>
);

20 Comments

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.
it only works if the array is always of the same length, if the length varies, your solution will not work.
@OlivierBoissé In the code above this would happen inside .map((el, i) => ....
@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.
You can omit that .fill(), just make sure you initiate the state with createRef: React.useState<RefObject<HTMLDivElement | null[]>>([createRef()])
|
102

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>

6 Comments

option two is what worked for me trying to use showCallout() on react-native-maps Markers
simple but helpful
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.
@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.
Option #1 will unnecessarily re-render all children by returning a new ref function on every render.
|
27

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.

4 Comments

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.
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.
Would be nice to improve it with complete removal of the key on unmount (not just setting it to null).
This is the way.
13

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>

Update:

Here is a Typescript version of the hook that creates memoized callback refs and supports passing a custom key to each element ref.

import { MutableRefObject, useRef } from 'react'

type UseElementsRefResult<T extends HTMLElement = HTMLElement> = [
  /**
   * Map of DOM node refs identified by an auto-created key or custom one.
   */
  MutableRefObject<{ readonly [K: string | number]: T }>,

  /**
   * Creates a memoized callback ref for a DOM node.
   * If you expect the rendering order of nodes to change, use a custom key.
   *
   * @param customKey Key used to set/unset the node ref.
   */
  (customKey?: string | number) => (el?: T) => void,
]

/**
 * Hook that creates callback refs to manage DOM node refs of multiple elements.
 * Ensures refs remain stable even with re-renders. Use custom keys if the rendering order may change.
 */
export const useElementsRef = <T extends HTMLElement = HTMLElement>(): UseElementsRefResult<T> => {
  const elementRefs = useRef({
    current: {} as { [K: string | number]: T },
    callbacks: {} as Record<string | number, (el?: T) => void>,
  })

  // Resets index on every render; stable refs as long as the render order remains unchanged
  let currentIndex = 0

  return [
    elementRefs.current,
    (customKey) => {
      const { current: elements, callbacks } = elementRefs.current
      const autoKey = currentIndex++
      const key = customKey ?? autoKey

      // Memoize callback for stable ref assignment
      if (!callbacks[key]) {
        callbacks[key] = (el) => {
          // React is setting the ref for this node
          if (el != null) {
            elements[key] = el
            return
          }

          // React is clearing the node ref
          delete elements[key]
        }
      }

      return callbacks[key]
    },
  ]
}

Demo

const { render } = ReactDOM
const { useEffect, useRef, useState } = React

const useElementsRef = () => {
  const elementRefs = useRef({
    current: {},
    callbacks: {},
  })

  let currentIndex = 0

  return [
    elementRefs.current,
    (customKey) => {
      const { current: elements, callbacks } = elementRefs.current
      const autoKey = currentIndex++
      const key = customKey ?? autoKey

      // Memoize callback for stable ref assignment
      if (!callbacks[key]) {
        callbacks[key] = (el) => {
          // React is setting the ref for this node
          if (el != null) {
            elements[key] = el
            return
          }

          // React is clearing the node ref
          delete elements[key]
        }
      }

      return callbacks[key]
    },
  ]
}

const App = () => {
  const [elementsRef, createElementRef] = useElementsRef()
  const [third, setThird] = useState(false)

  useEffect(() => {
    console.log(elementsRef.current)
  }, [third])

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

render(<App />, document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

2 Comments

This is good solution if you do have to have it one component
These are called a 'Callback Refs' - reactjs.org/docs/refs-and-the-dom.html#callback-refs
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 😉

Comments

9

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.

Comments

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

3 Comments

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

2 Comments

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 :)
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
5

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

Comments

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])

Comments

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.

1 Comment

I like this solution, it works and can easily be plugged in.
3

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'));

2 Comments

Another advantage using this approach if [] was a changing data source then everything would update in the natural React flow
Disadvantage is that the refs are only for internal use, and can't be used in the parent component.
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>  
    );
}

1 Comment

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.
3

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>
)

4 Comments

That looks neat! I was about to re-invent the already existing wheel. Thanks!
It's working great. Thanks!
It's worth noting that react-refs-collection uses useState to track the refs. This causes rerenders which goes against the paradigm of refs
@Turnip please, pay attention that this state never updates (setState is never called). State only stores map from id to input, and updates in this store never causes rerenders.
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/

Comments

1

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>

Comments

1

Proper way based on official guidelines

The answers here are a bit dated and require useEffect with side effects or other brittle approaches. There is no need for any of that. According to the official guidance, you can use "ref callbacks".

React will call your ref callback with the DOM node when it’s time to set the ref, and with null when it’s time to clear it. This lets you maintain your own array or a Map, and access any ref by its index or some kind of ID. e.g:

import { useRef } from "react";

const data = [{
    id: 1,
    text: 'test1'
  },{
    id: 2,
    text: 'test2'
  }
]

const Component = () => {
  const refs = useRef(new Map());

  return (
    <div>
      {data.map((item) => (
        <div
         key={item.id}
         ref={el => el ? refs.current.set(item.id, el) : refs.current.delete(item.id)}>
          {item.text}
        </div>
      ))}
    </div>
  );
};

If your array doesn't have a natural ID, you can always fall back to the good ol' index with similar caveats as using it in other places for a key.

1 Comment

Exactly. The official solution to this problem is described here: react.dev/learn/…
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();
}

1 Comment

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/…
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>
  );
};


Comments

0

I think it is cleaner and more real life condition solution when you work with real data and you need to address element by id:


const MyList = ()=>{
  const {listElements} = useListElements()
  
  const { getRefById, getItemRefFn } = useRefsList();
  const handleClick = (id: string)=>{
    getRefById(id)?.focus()
  }
  
  return <div>
    {listElements.map((item) => {
      const element_id = item?._id || '';
      
      return <div key={element_id} ref={getItemRefFn(element_id)} onClick={()=>handleClick(element_id)}>{element_id}</div>
    })}
  </div>
}

const useRefsList = () => {
  const refsList = useRef<Record<string, any>>({});
  const getItemRefFn = (_id: string) => (ref: any) => {
    refsList.current[_id] = ref;
  };
  const getRefById = (_id: string) => {
    return refsList.current[_id];
  };
  return { getItemRefFn, getRefById };
};

It also can be optimized for onClick handler and everything can be wrapped in callbacks (if necessary)

Comments

0

I prefer storing these in an object that can be stored and keyed by a unique ID. This reusable hook also ensures it is cleaned up when the hook is unmounted.

import { useCallback, useEffect, useRef } from "react";

export const useRefs = <T = HTMLElement>() => {
  const itemsRef = useRef<Record<string, T | null>>({});

  const setRef = useCallback((id: string, el: T | null) => {
    itemsRef.current[id] = el;
  }, []);

  const getRef = useCallback((id: string) => {
    if (!(id in itemsRef.current)) {
      console.warn(`id ${id} does not exist in itemsRef`);
      return null;
    }
    return itemsRef.current[id];
  }, []);

  const getAllRefs = useCallback(() => itemsRef.current ?? {}, []);

  useEffect(() => {
    // Clean up any extra references if needed
    itemsRef.current = { ...itemsRef.current };
    return () => {
      // Clean up when unmounted
      itemsRef.current = {};
    };
  }, []);

  return {
    getAllRefs,
    setRef,
    getRef,
  };
};

Comments

-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>
  );
};

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.