Build OTP Input Field in React — No Package Needed
Tags: webdev, javascript, react, frontend
Published: 2024-01-13 22:50:00

A lot of products are incorporating the idea of One-Time Password (or simply OTP) over sending users a link for them to click so they can be verified.

I'm not going to discuss the differences or advantages of using one over the other, but in this tutorial, you will learn how you can build the OTP Input component in your React application.

Previously, I have highlighted a step-by-step guide to building a tag input field without any package, you can [check it out here](https://dev.to/stephengade/create-tags-input-field-in-reactjs-s-no-package-required-5ae4).

If you're a front-end developer using other technologies like Vue.js, you can still learn by checking the logic of the component.

To ensure we are on the same page, this is an image showing what a typical OTP Input component looks like:

![OTP Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vclpnl0g4bq75zysq7p2.png)
Image credit: REDDIT

Now, let's break down our approach to solving this problem.

We will render input fried dynamically from the number of digits the OTP pin will be. For instance, if the OTP is 5 digits or characters, we will render 5 input. If it's 6 characters, we'll render 6 inputs, and so on.

When the user enters a character, the focus will be automatically set on the next and when it gets to the last one, we trigger the submit function or handle any logic we want to handle
Because we are human, we are prone to make mistakes. 

When our users make mistakes entering the OTP, they can delete or clear the characters to start again.

Let's start right away.

## Step 0: Bootstrap a React.js App

You can skip this step if you want to integrate into your existing project. But if you're starting afresh, you can Bootstrap a React.js application using Vite, Nextjs, or any framework of your choice.

To learn more about creating a React app, [check the official documentation](https://react.dev/learn/start-a-new-react-project).

I will be using Typescript and Tailwind CSS in this tutorial, kindly take note of that during your installation. You can skip Typescript since it's a small project. 

When you're done installing, `cd` into your project folder, clean up the default code, and get ready for the next step.

## Step 1: Create the OTP Component

In your component folder, create a new file called `OTPInput.tsx`, you can use `.jsx` if you don't want to use Typescript. 

Inside this file, add this code:

```jsx
import React, { useRef, useState, KeyboardEvent, ChangeEvent } from 'react';


type InputProps = {
    length?: number;
    onComplete: (pin: string) => void;
};
```

Note: if you're using Nextjs, don't forget to use `use client` at the top of your file. 

So far, all we have done is import the hooks we will be needing for our components. We then define the typing for the props we want to be lifting from this component. 

length: how many digits we want to accept
onComplete is a function that will be triggered immediately when the last number is entered.

Now, to the step.

## Step 2

Create a React component that lifts 2 props as defined above, and returns the input fields. Before we do that, think of a possible way we can render the input field.

Let's say you want to verify a User immediately when they sign up and you are sending 4 digits to the backend, you may think of creating 4 different input fields to handle the PIN CODE. 

But what if in the same project, you need to verify the same user when he went to change his password, but now the backend is sending an 8-digit PIN CODE, are you doing to create another `OTPInput` component again?

Well, it's not a smart idea. 

Let's use a simple JavaScript array method called `fill`... we just defined a length of props that's dynamic, right?

What we will do is to grab the length and fill it to an input field. For instance, if the length is 5, we fill it into an input and it becomes 5 input fields.

If you want to take 8 digits, it becomes 8 input fields, and so on without manually creating the 8 inputs.

So let's do just that:

```jsx
const OTPInput = ({ length = 4, onComplete }: InputProps) => {
    return (
        <div className='grid grid-cols-4 gap-5'>
            {Array.from({ length }, (_, InputIndex) => (
                <input
                    key={InputIndex}
                    type='text'
                    maxLength={1}
                    className={`border border-solid border-border-slate-500 focus:border-blue-600 p-5`}
                />
            ))}


        </div>
    )
}


export default OTPInput
```

At this junction, we are not doing another logic, just styling our input but note how we use the `Array.from(number)`. 

Also, note I'm using `grid-cols-4` meaning that the inputs should take 4 columns per role. You can use `flex` instead of grid or alter the grid column to fit your desired style.

But now let's keep track of the input field using the hooks we imported `useRef` and `useState`.

Before I return the input, I will have:

```jsx
//If you're not using Typescript, simply do const inputRef = useRef()


const inputRef = useRef<HTMLInputElement[]>(Array(length).fill(null));


// if you're not using Typescript, do useState()
const [OTP, setOTP] = useState<string[]>(Array(length).fill(''));


const handleTextChange = (input: string, index: number) => {
    const newPin = [...OTP];
    newPin[index] = input;
    setOTP(newPin);

    // check if the user has entered the first digit, if yes, automatically focus on the next input field and so on.


    if (input.length === 1 && index < length - 1) {
        inputRef.current[index + 1]?.focus();
    }

    if (input.length === 0 && index > 0) {
        inputRef.current[index - 1]?.focus();
    }

    // if user has entered all the digits, grab the digits and set as an argument to the onComplete function.


    if (newPin.every((digit) => digit !== '')) {
        onComplete(newPin.join(''));
    }
};
```
This is the basic logic block we need to handle the OTP Input component. But what do we have here?

We are using React's `useRef` hook to store and persist each of the input values without triggering any rerender. 

Then we are using the `const [OTP, setOTP] = useState()` to keep track of all the values.

Again, `useRef` for individual digits or input, and `useState` for all the digits.

The `handleTextChange` is expecting two arguments, the input and index of that input which we will pass in the returned JSX component.

In the function:

We grab the user input and set them in an array. So immediately a user types in something, it is stored in that array. And finally, it updates our OTP array using the `setOTP`.

Then we access each value by its index and save each indexed value as simply `input`.

The next three `if-statements` help us validate the input in real time and each validation is explained with a comment in the code.

Note that the `onComplete` props we passed is a function that we can trigger immediately after the input fields have been filled to submit the values to the backend.

Finally, we need to update the returned input component with our `ref` and `handleTextChange`.

The updated returned input is:

```jsx
// ….. previous code ….


return (
    <div className={`grid grid-cols-4 gap-5`}>
        {Array.from({ length }, (_, index) => (
            <input
                key={index}
                type="text"
                maxLength={1}
                value={OTP[index]}
                onChange={(e) => handleTextChange(e.target.value, index)}
                ref={(ref) => (inputRef.current[index] = ref as HTMLInputElement)}
                className={`border border-solid border-border-slate-500 focus:border-blue-600 p-5 outline-none`}
                style={{ marginRight: index === length - 1 ? '0' : '10px' }}
            />
        ))}
    </div>
);
```

The significant change here includes:

- `Key`: Since we are mapping an array, we need to specify a unique key to each input field that will be generated.
- `maxLength`: Each input field must take in a single value
- `VALUE`: If a user enters 2345 for example, we are setting the value of each input field to the entered OTP concerning their index. i.e.: the first field will have 2, the second will have 3, and so on.
- `onChange`: We are using the `handleTextChange` function here and we invoke the function whenever a user enters a value. 
- `ref`: we are keeping track of the active input field, remember we defined a `useRef()` hook above.

Now we are done. 

Let's stitch everything up, so here is the full `OTPInput.tsx` component.

```tsx
import React, { useRef, useState } from 'react';


// declare type for the props


type InputProps = {
  length?: number;
  onComplete: (pin: string) => void;
};


const OTPInput = ({ length = 4, onComplete }: InputProps) => {
  // if you're not using Typescript, simply do const inputRef = useRef()


  const inputRef = useRef<HTMLInputElement[]>(Array(length).fill(null));


  // if you're not using Typescript, do useState()
  const [OTP, setOTP] = useState<string[]>(Array(length).fill(''));


  const handleTextChange = (input: string, index: number) => {
    const newPin = [...OTP];
    newPin[index] = input;
    setOTP(newPin);


    // check if the user has entered the first digit, if yes, automatically focus on the next input field and so on.


    if (input.length === 1 && index < length - 1) {
      inputRef.current[index + 1]?.focus();
    }


    if (input.length === 0 && index > 0) {
      inputRef.current[index - 1]?.focus();
    }


    // if the user has entered all the digits, grab the digits and set as an argument to the onComplete function.


    if (newPin.every((digit) => digit !== '')) {
      onComplete(newPin.join(''));
    }
  };


  // return the inputs component


  return (
    <div className={`grid grid-cols-4 gap-5`}>
      {Array.from({ length }, (_, index) => (
        <input
          key={index}
          type="text"
          maxLength={1}
          value={OTP[index]}
          onChange={(e) => handleTextChange(e.target.value, index)}
          ref={(ref) => (inputRef.current[index] = ref as HTMLInputElement)}
          className={`border border-solid border-border-slate-500 focus:border-blue-600 p-5 outline-none`}
          style={{ marginRight: index === length - 1 ? '0' : '10px' }}
        />
      ))}
    </div>
  );
};


export default OTPInput;
```

We can now use the component in our project but don't forget to pass values to the `length` and handle the submit event to trigger to invoke the `onComplete` props.

Here is an example to use the component.

In `page` folder Create a new file called `VerifyUser.tsx` and put this code:

```tsx
// import the OTP Input component


import React from 'react'
import OTPInput from 'the-path'


const VerifyUser = () => {
    // handle OTP Submit
    const handleSubmit = (pin: string) => {
        // handle api request here but I'm console logging it
        console.log(pin)
    }


    return (
        <section className='h-screen w-screen flex flex-col justify-center items-center'>
            <h2>Verify OTP</h2>


            <p>An OTP has been sent to your email address, kindly enter them here</p>


            <OTPInput length={5} onComplete={handleSubmit} />


        </section>
    )
}


export default VerifyUser
```

## What's happening:

Note that we are passing `5` as our length, meaning we will have 5 input fields for the OTP.

Then we simply pass `handleSubmit` to the `onComplete` props. Note that in our `OTPInput.tsx`, we have passed the user-entered input values as arguments to the `onComplete` function. We then invoke that function with `handleSubmit`

That's everything and it should be working as expected. You can of course modify the design and logic to suit your project's needs.

If you have any questions, I will be glad to answer as much as possible. Thank you for reading.