How to target DOM with react useRef in map

React JS Faysal Shuvo

Problem:

You are working on a website. And now you want to get an array of DOM elements using the useRef() hook. In this article, we are going to learn how to target DOM with react useRef in the map.

example:

const App = () => 
{
  // In `data`, I would like to get an array of DOM element
  let data = useRef(null);
  return <div>
    {['top', 'bottom'].map((el, i) =>
      <li key={i} ref={data} children={el} />
    )}
  </div>
}


Solution:

React's ref is similar to useRef(just structure of object with an only field of current). Unlike useState, useRef stores data between renders, and it does not trigger re-rendering while changing that data. As always it is better to avoid initializing hook in loops or if statements.

So this is how you do it:

1. First create array and keep it between renders by useRef

2. By using createRef() we initialize each array's element

3. and by using .current notation we refer to list.

const App = () => {
    let refs = useRef([React.createRef(), React.createRef()]);  
    useEffect(() => {
      refs.current[0].current.focus()
    }, []);
    return (<div>
      {['top', 'bottom'].map((el, i) =>
        <li key={i}><input ref={refs.current[i]} value={el} /></li>
      )}
    </div>)
  }

This way we can modify the array(by changing its length). And mutating data stored by useRef does not re-render. So we need to involve useState to re-render by changing the length.

  const App = () => {
    const [duration, setDuration] = useState(2);
    const refs = useRef([React.createRef(), React.createRef()]);
    function updateDurationHandler({ target: { value }}) {
      setDuration(value);
      refs.current = refs.current.splice(0, value);
      for(let i = 0; i< value; i++) {
        refs.current[i] = refs.current[i] || React.createRef();
      }
      refs.current = refs.current.map((item) => item || React.createRef());
    }
     useEffect(() => {
     refs.current[refs.current.duration - 1].current.focus()
    }, [duration]);
  
    return (<>
      <div>
      {refs.current.map((el, i) =>
        <li key={i}><input ref={refs.current[i]} value={i} /></li>
      )}
    </div>
    <input value={refs.current.duration} type="number" onChange={updateDurationHandler} />
    </>)
  }

Never try to access refs.current[0].current at first rendering because it will raise an error.

say

return (<div>
    {['top', 'bottom'].map((el, i) =>
      <li key={i}>
        <input ref={refs.current[i]} value={el} />
        {refs.current[i].current.value}</li> // cannot read property `value` of undefined
    )}
  </div>)

or

return (<div>
    {['top', 'bottom'].map((el, i) =>
      <li key={i}>
        <input ref={refs.current[i]} value={el} />
        {refs.current[i].current && refs.current[i].current.value}</li> // cannot read property `value` of undefined
    )}
  </div>)

The reason behind this is: ref is bound after an element is rendered so during rendering is running for the first time it is not initialized yet.


Thank you for reading this article. Hopefully, the solutions provided will solve your problem.