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

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

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>

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

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>