In today's world, where cybersecurity threats are on the rise, one-time passcodes (OTPs) have become a popular security measure to protect user accounts from unauthorized access. However, the inconvenience of inputting these codes has become a source of frustration for many users. As a result, facilitating an easy input process for OTPs has become increasingly important. By simplifying the input process, users can enjoy the benefits of increased security without feeling inconvenienced. This can lead to improved user satisfaction and increased adoption of security measures, ultimately helping to protect both users and businesses from potential security breaches.
In this article, I'll show you how to create a robust OTP code input component for React.js, using no other dependencies. Although for my example, I am using tailwindcss
and react-icons
, but they are totally optional.
The Component
Here's the code for our EnterCode
component:
import React, { useRef, useState, useEffect } from 'react';
import { FaTimes } from 'react-icons/fa'
export default function EnterCode({ callback, reset, isLoading }) {
const [code, setCode] = useState('');
// Refs to control each digit input element
const inputRefs = [
useRef(null),
useRef(null),
useRef(null),
useRef(null),
useRef(null),
useRef(null),
];
// Reset all inputs and clear state
const resetCode = () => {
inputRefs.forEach(ref => {
ref.current.value = '';
});
inputRefs[0].current.focus();
setCode('');
}
// Call our callback when code = 6 chars
useEffect(() => {
if (code.length === 6) {
if (typeof callback === 'function') callback(code);
resetCode();
}
}, [code]); //eslint-disable-line
// Listen for external reset toggle
useEffect(() => {
resetCode();
}, [reset]); //eslint-disable-line
// Handle input
function handleInput(e, index) {
const input = e.target;
const previousInput = inputRefs[index - 1];
const nextInput = inputRefs[index + 1];
// Update code state with single digit
const newCode = [...code];
// Convert lowercase letters to uppercase
if (/^[a-z]+$/.test(input.value)) {
const uc = input.value.toUpperCase();
newCode[index] = uc;
inputRefs[index].current.value = uc;
} else {
newCode[index] = input.value;
}
setCode(newCode.join(''));
input.select();
if (input.value === '') {
// If the value is deleted, select previous input, if exists
if (previousInput) {
previousInput.current.focus();
}
} else if (nextInput) {
// Select next input on entry, if exists
nextInput.current.select();
}
}
// Select the contents on focus
function handleFocus(e) {
e.target.select();
}
// Handle backspace key
function handleKeyDown(e, index) {
const input = e.target;
const previousInput = inputRefs[index - 1];
const nextInput = inputRefs[index + 1];
if ((e.keyCode === 8 || e.keyCode === 46) && input.value === '') {
e.preventDefault();
setCode((prevCode) => prevCode.slice(0, index) + prevCode.slice(index + 1));
if (previousInput) {
previousInput.current.focus();
}
}
}
// Capture pasted characters
const handlePaste = (e) => {
const pastedCode = e.clipboardData.getData('text');
if (pastedCode.length === 6) {
setCode(pastedCode);
inputRefs.forEach((inputRef, index) => {
inputRef.current.value = pastedCode.charAt(index);
});
}
};
// Clear button deletes all inputs and selects the first input for entry
const ClearButton = () => {
return (
<button
onClick={resetCode}
className="text-2xl absolute right-[-30px] top-3"
>
<FaTimes />
</button>
)
}
return (
<div className="flex gap-2 relative">
{[0, 1, 2, 3, 4, 5].map((index) => (
<input
className="text-2xl bg-gray-800 w-10 flex p-2 text-center"
key={index}
type="text"
maxLength={1}
onChange={(e) => handleInput(e, index)}
ref={inputRefs[index]}
autoFocus={index === 0}
onFocus={handleFocus}
onKeyDown={(e) => handleKeyDown(e, index)}
onPaste={handlePaste}
disabled={isLoading}
/>
))}
{
code.length
?
<ClearButton />
:
<></>
}
</div>
);
}
Let's break this down:
The component accepts 3 arguments:
- callback: function to call when code reaches 6 digits
- reset: a boolean state to toggle when you want to reset the component externally
- isLoading: boolean toggle to disable inputs
The first useEffect()
waits for the code
to reach 6 characters and then sends it to our callback function. The second one listens for the state of reset
to change and then resets our component accordingly.
Our handleInput()
function handles setting the state of code, advancing to the next input, and converts all lowercase letters to uppercase.
The handleFocus()
function selects the contents of the input when it is focused. This makes for a better user experience--especially mobile users.
The handleKeyDown()
function listens for the backspace or delete keys and selects the previous box if detected.
The handlePaste()
function captures pasted text in any one of the inputs and then updates our code state and then splits the characters into each input box.
Finally, we have a ClearButton
component that shows when there are 1 or more digits in the input. Clicking it resets the component.
Example Usage
Here's an example of how you might implement this component:
import React, { useState } from "react";
import EnterCode from "@/components/Forms/EnterCode";
export default function VerifyCodePage() {
const [isLoading, setIsLoading] = useState(false);
const handleCodeSubmit = async (code) => {
if (isLoading) return;
try {
const payload = new FormData();
payload.append("code", code);
const result = await fetch("/path/to/api/endpoint", {
method: "POST",
body: payload,
});
if (!result.ok) {
const mess = await result.text();
throw new Error(mess);
}
alert("Code is verified!");
} catch (err) {
alert(`Error: ${err.message}`);
} finally {
setIsLoading(false);
}
return (
<div className="flex flex-col gap-6">
<EnterCode isLoading={isLoading} callback={handleCodeSubmit} />
</div>
);
};
}
Thank you for taking the time to read my article and I hope you found it useful (or at the very least, mildly entertaining). For more great information about web dev, systems administration and cloud computing, please read the Designly Blog. Also, please leave your comments! I love to hear thoughts from my readers.
I use Hostinger to host my clients' websites. You can get a business account that can host 100 websites at a price of $3.99/mo, which you can lock in for up to 48 months! It's the best deal in town. Services include PHP hosting (with extensions), MySQL, Wordpress and Email services.
Looking for a web developer? I'm available for hire! To inquire, please fill out a contact form.
Top comments (0)