Creating a Form in React with React Hook Form and Next.js 13.4
Tags: nextjs, react, tailwindcss, reacthookform
Published: 2023-10-13 11:17:51


In this tutorial, we will create a login form in a Next.js application using React Hook Form for form handling and validation. We will also use the Zod library to define and validate the form schema. By the end of this tutorial, you will have a fully functional login form with client-side validation.

# Prerequisites

Before we begin, ensure you have the following installed:

1.  Node.js and npm (Node Package Manager)
2.  Next.js 13.4 or later

# Project Setup

Let’s set up our Next.js project and install the necessary dependencies.

1.  Create a new Next.js project by running the following command:

```bash
npx create-next-app login-form-app  
cd login-form-app
```

2. Install the required packages for form handling and validation:

```bash
npm install react-hook-form @hookform/resolvers zod
```

# Creating the Form

In this example, we’ll create a login form that takes an email and password as input fields.

Create a new file called  `LoginForm.tsx`  in the  `src`  directory with the following content:

```ts
// app/components/LoginForm.tsx  
"use client";  
import { zodResolver } from "@hookform/resolvers/zod";  
import { useRouter } from "next/router";  
import { useForm } from "react-hook-form";  
import * as z from "zod";  
import { loginSchema } from "../zodSchema/login";  
  
type FormData = z.infer<typeof loginSchema>;  
  
export default function LoginForm() {  
  const router = useRouter(); const {  
    handleSubmit,  
    register, formState: { errors, isSubmitting, isDirty, isValid },  
  } = useForm<FormData>({ resolver: zodResolver(loginSchema),  
  });  
  
  async function onSubmit(data: FormData) {  
    console.log(isSubmitting);  
    console.log(data);  
    // Replace this with a server action or fetch an API endpoint to authenticate  
    await new Promise<void>((resolve) => {  
      setTimeout(() => {  
        resolve();  
      }, 2000); // 2 seconds in milliseconds  
    });  
    router.push("/tweets");  
  }  
  
  return (  
    <div className="selection:bg-rose-500 selection:text-white">  
      <div className="flex min-h-screen items-center justify-center bg-rose-100">  
        <div className="flex-1 p-8">  
          <div className="mx-auto w-80 overflow-hidden rounded-3xl bg-white shadow-xl">  
            {/* Form Header */}  
            <div className="rounded-bl-4xl relative h-44 bg-rose-500">  
              <svg  
                className="absolute bottom-0"  
                xmlns="http://www.w3.org/2000/svg"  
                viewBox="0 0 1440 320"  
              >  
                <path  
                  fill="#ffffff"  
                  fillOpacity="1"  
                  d="M0,64L48,80C96,96,192,128,288,128C384,128,480,96,576,85.3C672,75,768,85,864,122.7C960,160,1056,224,1152,245.3C1248,267,1344,245,1392,234.7L1440,224L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"  
                ></path>  
              </svg>  
            </div>  
  
            {/* Form Body */}  
            <div className="rounded-tr-4xl bg-white px-10 pb-8 pt-4">  
              <h1 className="text-2xl font-semibold text-gray-900">  
                Welcome back!  
              </h1>  
              <form  
                className="mt-12"  
                action=""  
                method="POST"  
                onSubmit={handleSubmit(onSubmit)}  
              >  
                {/* Email Input */}  
                <div className="relative">  
                  <input  
                    {...register("email", { required: true })}  
                    id="email"  
                    name="email"  
                    type="text"  
                    className="peer h-10 w-full border-b-2 border-gray-300 text-gray-900 placeholder-transparent focus:border-rose-600 focus:outline-none"  
                    placeholder="john@doe.com"  
                    autoComplete="off"  
                  />  
                  {errors?.email && (  
                    <p className="text-red-600 text-sm">  
                      {errors?.email?.message}  
                    </p>  
                  )}  
                  <label  
                    htmlFor="email"  
                    className="absolute -top-3.5 left-0 text-sm text-gray-600 transition-all peer-placeholder-shown:top-2 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-400 peer-focus:-top-3.5 peer-focus:text-sm peer-focus:text-gray-600"  
                  >  
                    Email address  
                  </label>  
                </div>  
  
                {/* Password Input */}  
                <div className="relative mt-10">  
                  <input  
                    {...register("password", { required: true })}  
                    id="password"  
                    type="password"  
                    name="password"  
                    className="peer h-10 w-full border-b-2 border-gray-300 text-gray-900 placeholder-transparent focus:border-rose-600 focus:outline-none"  
                    placeholder="Password"  
                    autoComplete="off"  
                  />  
                  {errors?.password && (  
                    <p className="text-red-600 text-sm">  
                      {errors?.password?.message}  
                    </p>  
                  )}  
                  <label  
                    htmlFor="password"  
                    className="absolute -top-3.5 left-0 text-sm text-gray-600 transition-all peer-placeholder-shown:top-2 peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-400 peer-focus:-top-3.5 peer-focus:text-sm peer-focus:text-gray-600"  
                  >  
                    Password  
                  </label>  
                </div>  
  
                {/* Submit Button */}  
                <button  
                  type="submit"  
                  disabled={!isDirty || !isValid || isSubmitting}  
                  className="mt-20 block w-full cursor-pointer rounded bg-rose-500 px-4 py-2 text-center font-semibold text-white hover:bg-rose-400 focus:outline-none focus:ring focus:ring-rose-500 focus:ring-opacity-80 focus:ring-offset-2 disabled:opacity-70"  
                >  
                  {isSubmitting ? (  
                    <div role="status">  
                      <svg  
                        aria-hidden="true"  
                        className="inline w-6 h-6 mr-2 text-white animate-spin fill-rose-600 opacity-100"  
                        viewBox="0 0 100 101"  
                        fill="none"  
                        xmlns="http://www.w3.org/2000/svg"  
                      >  
                        {/* SVG for Spinner Animation */}  
                      </svg>  
                    </div>  
                  ) : (  
                    "Sign In"  
                  )}  
                </button>  
              </form>  
  
              {/* Forgot Password Link */}  
              <a  
                href="#"  
                className="mt-4 block text-center text-sm font-medium text-rose-600 hover:underline focus:outline-none focus:ring-2 focus:ring-rose-500"  
              >  
                Forgot your password?  
              </a>  
            </div>  
          </div>  
        </div>  
      </div>  
    </div>  
  );  
}
```
Now that we have created the  `LoginForm`  component, we can import and use it in our main page component.

Create a new file called  `index.tsx`  in the  `pages`  directory with the following content:

```ts
// app/page.tsx  
  
import LoginForm from "../src/components/LoginForm";  
  
export default function HomePage() {  
  return (  
      <LoginForm />  
  );  
}
```
In this file, we have imported the  `LoginForm`  component and rendered it inside the  `HomePage`  component.

# Zod Schema

Next, let’s define the  `loginSchema`  using Zod for form validation. Create a new file called  `login.ts`  in the  `src/zodSchema`  directory with the following content:
```ts
// src/zodSchema/login.ts  
  
import z from "zod";  
  
export const loginSchema = z.object({  
  email: z.string().email(),  
  password: z.string().min(8),  
});  
  
export type User = z.infer<typeof loginSchema>;
```
This schema defines the shape of the data we expect from the login form fields.

# Testing the Form

Now,  we have a fully functional login form with client-side validation. The form will check if the email is in the correct format and if the password has a minimum length of 8 characters before allowing submission.

To test the form, run your Next.js application:

`npm run dev`

Visit  `http://localhost:3000`  in your browser to see the login form in action.

# Summary

In this tutorial, we learned how to create a login form in a Next.js application using React Hook Form for form handling and validation. We used Zod to define and validate the form schema. The form will display error messages if the user enters invalid data, and it will disable the submit button until the form is filled correctly. This implementation provides a good starting point for creating more complex forms in your React and Next.js projects.

Remember that client-side validation is a great way to improve user experience, but it should not be the only layer of security for a login form. Always ensure that you have robust server-side validation and authentication mechanisms to protect your application from potential security threats.

Happy coding! 🚀