Designly Blog

How to Create a Verification Code Input Component in React / Next.js

How to Create a Verification Code Input Component in React / Next.js

Posted in Front End Development by Jay Simons
Published on April 9, 2023

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:

  1. callback: function to call when code reaches 6 digits
  2. reset: a boolean state to toggle when you want to reset the component externally
  3. 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.


Loading comments...