Wednesday, 3 January, 2024 UTC


Summary

Recently I needed to add something similar to a pincode in a React application. Something like when you enter your pin at the ATM, only that this one could accept also letters, not just numbers.
And of course, I ended up reinventing the wheel, instead of just using a built-in component 🤦‍♂️.
So, in today's article, I will share my approach.
Defining the React pincode component
The requirements:
  • we can pass it a property to set the characters length of the pin
  • it will receive a callback function that will be called when the pin was fully typed
  • if we don't start on the first digit, we are forced to the first one
  • it will auto-focus the next digit after a digit was typed
The end result will work as in this video:
React components Structure
We will have two React components:
1. the actual <Pincode> component that will receive the callback function for when the pin was fully typed and the total length of the pin:
const PinCode = ({ length, onPinEntered }) => { }

const App = ()=> {
  return (
    <div className="App">
      <PinCode 
            length={5} 
            onPinEntered={(pin) => alert("Pin : " + pin)} />
    </div>
  );
}
2. the <Digit> component will also have a callback function for when a digit is typed, a ref object, and a num index value that we will use to move and focus the next digit:
const Digit = forwardRef(({ num, onDigitAdded }, ref) => {})
Building the React pincode component
The first step will be just to render the digits inputs in a loop, based on the wanted length of the final pin:
const PinCode = ({ length, onPinEntered }) => {
  let [pin, setPin] = useState("");
  const onDigitAdded = (d) => {
    setPin(pin + d.value);
    focusNextDigit(d);
  };

  return (
    <div onClick={onClickHandler}>
      {[...Array(length).keys()].map((i) => (
        <Digit
          key={i}
          num={i}
          onDigitAdded={onDigitAdded}
        />
      ))}
    </div>
  );
};
At this point the component will look as follows:
Using the onDigitAdded() function we add digits to the state variable pin.
When the pin var has the same size as the lenght property of the PinCode component then it means we have fully typed the pin and we will pass it to the callback function.
const PinCode = ({ length, onPinEntered }) => {
  // ...
  useEffect(() => {
    if (pin.length === length) {
      onPinEntered(pin);
    }
  }, [pin, onPinEntered, length]);
}
The overall structure of the Digit component is pretty simple:
const Digit = forwardRef(({ num, onDigitAdded }) => {
  const keyUpHandler = (e) => onDigitAdded(e.target);
  return (
    <input
      onKeyUp={keyUpHandler}
      type="password"
      maxLength="1"
      data-index={num}
    />
  );
});
Working with multiple React refs to manage the focus of the PinCode Component
You may wonder why the Digit component is wrapped in the forwardRef() hook!?
Well, initially my first attempt was to use the querySelectorAll() function to manage the focus of the multiple-digit inputs.
However, realized that it's not a good idea to use querySelectorAll() in React as it will mess up the Virtual DOM.
Therefore, the next try was to add refs to each Digit component:
const PinCode = ({ length, onPinEntered }) => {
  const digits = useRef([]);
  // ...
  return (
    <div onClick={onClickHandler}>
      {[...Array(length).keys()].map((i) => (
        <Digit
          ref={(d) => digits.current.push(d)}
    // ...
  );
And now we can implement the focusNextDigit() function in the PinCode component:
const focusNextDigit = (d) => {
    const index = parseInt(d.dataset.index);
    const nextDigit = digits.current[index + 1];
    nextDigit?.focus();
}; 
And also, if the user does not start on the first digit, we will auto-move it there:
const onClickHandler = () => {
    if (pin !== "") {
      setPin("");
      digits.current.forEach((d) => {
        if (d) d.value = "";
      });
    }
    digits.current[0].focus();
};
Wrote more here about how to use an array of components with the useRef() hook.
At this point, all the functionality should work as expected.
Our example can be improved by adding support for deleting characters or for pasting codes from the clipboard. In order to be able to paste codes in this component you can use this post as a starting point, and I've also written an intro on how to add copy-paste support for React components.
Styling the PinCode React Component
The final step will be to add some CSS. Fortunately, this step was pretty simple:
body {
  font-family: sans-serif;
  text-align: center;
  background-color: orangered;
}

input {
  width: 50px;
  height: 50px;
  margin: 0.5rem;
  padding: 0.5rem;
  border: 2px solid white;
  text-align: center;
  font-size: 5rem;
  color: white;
  background-color: orangered;
}
After this, we will the final output:
Full code and live example
You can checkout on my Github the full code and see the live example here.