Saturday, 20 January, 2024 UTC


Summary

While working on the React pincode field component, I came to a point where I needed to store the references to an array of objects.
While this is an easy task for documentSelectorAll, it turns out that is not a good idea to use documentSelectorAll() in a React component.
Basically needed a way to use the React useRef() hook with an array of components objects.
How to use the useRef() hook with an array
As an example will want to build the below React app:
Each time the user clicks on a button, the next available input is selected. If the user clicks on the last button, the first input is selected. See the below video:
In order to have the useRef() hook work with an array, the first step will be to instantiate the hook with an empty array, instead of the null value:
const inputs = useRef([]);
In order to populate the references array with items we will pass a function to the useRef() hook. This function will push items to the array, while they are created:
<input ref={(i) => inputs.current.push(i)} />
And now if we want to access a given element in the array we can just use thecurrent property:
{[...Array(5).keys()].map((i) => (
    inputs.current[index]?.focus()
))}
The full code is below:
import { useRef } from "react";

export default function App() {
  const inputs = useRef([]);
  const onFocusNextInput = (i) => {
    let nextIndex = i + 1 >= inputs.current.length ? 0 : i + 1;
    inputs.current[nextIndex]?.focus();
  };
  return (
    <div className="App">
      {[...Array(5).keys()].map((i) => (
        <div key={i}>
          <input ref={(i) => inputs.current.push(i)} />
          <button onClick={() => onFocusNextInput(i)}>Focus next input</button>
        </div>
      ))}
    </div>
  );
}
Using useRef() with an array of React components via the forwardRef() hook
We may want to extract separate React components for more complex situations.
For example, we want to extract the button and the input in their own component:
const SpecialInput =({ onClickHandler }) => (
  <div>
    <input />
    <button onClick={onClickHandler}>Focus next input</button>
  </div>
));
In cases like this, we need to find a way to pass the reference form the array to that component. Therefore, we will need to use the forwardRef() hook.
Luckily it's quite easy to adapt the example for useRef() with an array to include also the usage of forwardRef():
import { useRef, forwardRef } from "react";

const SpecialInput = forwardRef(({ onClickHandler }, ref) => (
  <div>
    <input ref={ref} />
    <button onClick={onClickHandler}>Focus next input</button>
  </div>
));

export default function App() {
  const inputs = useRef([]);
  const onFocusNextInput = (i) => {
    let nextIndex = i + 1 >= inputs.current.length ? 0 : i + 1;
    inputs.current[nextIndex]?.focus();
  };
  return (
    <div className="App">
      {[...Array(5).keys()].map((i) => (
        <SpecialInput key={i}
          ref={(i) => inputs.current.push(i)}
          onClickHandler={() => onFocusNextInput(i)}
        />
      ))}
    </div>
  );
}
You can check out here the full working example.
Also you can you may be interested in how to scroll into view the last item added in a React list.