React One Time Password Input Component
I recently had an opportunity to create a one-time-password like component in a React app. The goal was to have a readonly separated, 6 digit/character code that can be displayed on one device and a separated input to enter that code in a web app.
This is the solution that worked for our case and I would love to hear others have solved this challenge as well.
What We're Building
The component that is orchestrating the
SeparatedInput component will need to have a state for the value and a handler to update that state with the value that
SeparatedInput will pass back up. We can set it up like this:
Rendering The Inputs
We're going to start with a new component and name it something like
SeparatedInput.tsx. This component will house our inputs and the handlers for them. We'll be using styled-components to make the styling a bit easier.
We'll need to pass our value to this component so let's add that as a prop.
Now we need to separate this value into an array that we can map into our inputs.
We can spread our value into an array to create an array of characters. This is great if our
value is 6 characters long but doesn't quite work if it's less than that. To fix this, we can fill out the rest with empty strings using
Array(n).fill(""). We can then spread this into our array as well.
resolvedValue.length = MAX_DIGITS; trims any elements over our length of
From here, we can map over our array and render our inputs using those strings.
Handling Input Value Changes
We can use one function to pass to the inputs since each input will send an event on change. With this, we'll be passing our
onChange back up via a prop.
Great! This handler will take each unique event and add that to the value. We still have an issue with typing into the inputs since they don't advance to the next input. This will cause the value to keep growing out of sync with the focussed input.
For the focus we can leverage
useRef. We'll get the ref of the
InputContainer so we can control the focus of its children. We'll also need some state to handle the focus index. We can then update the focus via
useEffect on the update of focus index.
To set the focus index we can take the previous state and check to see if it's less than the last input index and, if so, increment by one.
Cool, this seems to work pretty well for adding characters to the inputs. The next thing we'll need to do is handle removing characters using "Backspace." To achieve this, we'll use the useKeyPressEvent hook. This hook takes the keyPress key as the first argument and a callback function to do something when that key is pressed.
Let's set up the callback function first. For our specific case we want the "Backspace" key remove the last character in our value. We can use slice on the value string to handle this. After removing the item, we'll set the focus to the previous input.
Once our function is in place we can pass it to
Now we can move back and forth by typing characters and "Backspace."
We can still click into any input and the focus will shift to that one. However, if we type it will add the character to the next input in our sequence. There are few ways to improve this experience and I'll share the one that is currently meeting our needs, though we may update it in the future.
The input sequence we'll use is just forward and back, without the user clicking or tabbing/shift+tabbing into other inputs. Since it's only six characters it shouldn't be too bad to hit "Backspace" a few times rather than update a previous input via mouse click. If it were longer I would definitely opt for a solution where you can correct any input in the chain.
We can use the
disabled attribute on the inputs to prevent the input from receiving focus. We'll create a check to make sure that the inputs before and after our current focus index are disabled. We'll also prevent it from disabling the last input if all values are filled.
Display the Code
This component will also be used to display the generated code to the user in the other device. In order to use this as a display component we'll want to make sure the user can't update the inputs. We'll use a combination of
The parent can pass the
value and the
readOnly props to the
SeparatedInput will then use this to check for in the handlers and inputs to make sure it will only show the value, not change it. The final component code will then be as follows.