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:
- Node.js and npm (Node Package Manager)
- Next.js 13.4 or later
Project Setup
Let’s set up our Next.js project and install the necessary dependencies.
- Create a new Next.js project by running the following command:
npx create-next-app login-form-app
cd login-form-app
- Install the required packages for form handling and validation:
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:
// 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:
// 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:
// 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! 🚀