Skip to main content

Next.js React Hook Form

Forms are a fundamental part of web applications, allowing users to input data and interact with your application. While HTML provides native form elements, managing form state, validation, and submission in React applications can become complex. This is where React Hook Form comes in - a performant, flexible, and extensible forms library with easy-to-use validation for React applications.

In this guide, we'll explore how to integrate React Hook Form with Next.js to create efficient, user-friendly forms with built-in validation.

Introduction to React Hook Form

React Hook Form is a popular library that simplifies form handling in React applications. It leverages React's hooks system to provide a lightweight solution with minimal re-renders.

Key advantages of using React Hook Form:

  • Performance: Minimizes component re-renders
  • Less code: Reduces boilerplate compared to traditional form handling
  • Easy validation: Built-in validation with straightforward error handling
  • No dependencies: Lightweight with zero dependencies

Getting Started with React Hook Form in Next.js

Step 1: Installation

First, let's install React Hook Form in your Next.js project:

bash
npm install react-hook-form
# or
yarn add react-hook-form

Step 2: Creating a Basic Form

Let's create a simple login form using React Hook Form:

jsx
import { useForm } from 'react-hook-form';

export default function LoginForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm();

const onSubmit = (data) => {
console.log(data);
// Handle form submission (e.g., API call)
};

return (
<div className="form-container">
<h1>Login</h1>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register("email", {
required: "Email is required",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address"
}
})}
/>
{errors.email && <p className="error">{errors.email.message}</p>}
</div>

<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
{...register("password", {
required: "Password is required",
minLength: {
value: 8,
message: "Password must be at least 8 characters"
}
})}
/>
{errors.password && <p className="error">{errors.password.message}</p>}
</div>

<button type="submit">Login</button>
</form>
</div>
);
}

Understanding the Code

Let's break down the key elements of React Hook Form:

  1. useForm Hook: This is the main hook from React Hook Form that provides form functionality.

  2. register: A method that registers an input with the form. It returns props to be spread onto the input element.

  3. handleSubmit: A function that receives the form data if form validation is successful.

  4. formState: An object containing form state information, including any validation errors.

The {...register("fieldName", validationOptions)} syntax is a spread operator that applies all the necessary props to make an input work with React Hook Form.

Form Validation

React Hook Form provides several built-in validation options:

jsx
// Basic required validation
{...register("firstName", { required: "First name is required" })}

// String length validation
{...register("username", {
minLength: { value: 3, message: "Username must be at least 3 characters" },
maxLength: { value: 20, message: "Username cannot exceed 20 characters" }
})}

// Pattern validation (regex)
{...register("email", {
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address"
}
})}

// Custom validation
{...register("password", {
validate: (value) =>
value.includes("123") ? "Password cannot contain 123" : true
})}

Real-World Example: Registration Form

Let's create a more comprehensive registration form that showcases more features of React Hook Form:

jsx
import { useForm } from 'react-hook-form';
import { useState } from 'react';
import styles from './RegistrationForm.module.css';

export default function RegistrationForm() {
const {
register,
handleSubmit,
formState: { errors },
watch,
reset
} = useForm();

const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);

// Watch password field for confirmation validation
const password = watch("password");

const onSubmit = async (data) => {
setIsSubmitting(true);

try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
console.log("Form submitted successfully:", data);

// Reset form after successful submission
reset();
setSubmitSuccess(true);
} catch (error) {
console.error("Error submitting form:", error);
} finally {
setIsSubmitting(false);
}
};

return (
<div className={styles.formContainer}>
<h1>Create an Account</h1>

{submitSuccess ? (
<div className={styles.successMessage}>
<h2>Registration Successful!</h2>
<p>Your account has been created successfully.</p>
<button onClick={() => setSubmitSuccess(false)}>Register Another Account</button>
</div>
) : (
<form onSubmit={handleSubmit(onSubmit)}>
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
{...register("firstName", {
required: "First name is required"
})}
/>
{errors.firstName && <p className={styles.error}>{errors.firstName.message}</p>}
</div>

<div className={styles.formGroup}>
<label htmlFor="lastName">Last Name</label>
<input
id="lastName"
{...register("lastName", {
required: "Last name is required"
})}
/>
{errors.lastName && <p className={styles.error}>{errors.lastName.message}</p>}
</div>
</div>

<div className={styles.formGroup}>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register("email", {
required: "Email is required",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "Invalid email address"
}
})}
/>
{errors.email && <p className={styles.error}>{errors.email.message}</p>}
</div>

<div className={styles.formGroup}>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
{...register("password", {
required: "Password is required",
minLength: {
value: 8,
message: "Password must be at least 8 characters"
},
validate: {
hasUpperCase: (value) =>
/[A-Z]/.test(value) || "Password must contain at least one uppercase letter",
hasLowerCase: (value) =>
/[a-z]/.test(value) || "Password must contain at least one lowercase letter",
hasNumber: (value) =>
/[0-9]/.test(value) || "Password must contain at least one number",
hasSpecialChar: (value) =>
/[!@#$%^&*(),.?":{}|<>]/.test(value) || "Password must contain at least one special character"
}
})}
/>
{errors.password && <p className={styles.error}>{errors.password.message}</p>}
</div>

<div className={styles.formGroup}>
<label htmlFor="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
{...register("confirmPassword", {
required: "Please confirm your password",
validate: (value) => value === password || "Passwords do not match"
})}
/>
{errors.confirmPassword && <p className={styles.error}>{errors.confirmPassword.message}</p>}
</div>

<div className={styles.formGroup}>
<label>
<input
type="checkbox"
{...register("termsAccepted", {
required: "You must accept the terms and conditions"
})}
/>
I accept the terms and conditions
</label>
{errors.termsAccepted && <p className={styles.error}>{errors.termsAccepted.message}</p>}
</div>

<button
type="submit"
disabled={isSubmitting}
className={isSubmitting ? styles.submitting : ''}
>
{isSubmitting ? 'Registering...' : 'Register'}
</button>
</form>
)}
</div>
);
}

To style this form, you can create a RegistrationForm.module.css file:

css
.formContainer {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.formRow {
display: flex;
gap: 20px;
}

.formGroup {
margin-bottom: 20px;
flex: 1;
}

.formGroup label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}

.formGroup input:not([type="checkbox"]) {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}

.error {
color: #e74c3c;
font-size: 14px;
margin-top: 5px;
}

.formContainer button {
background-color: #3498db;
color: white;
padding: 12px 20px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}

.formContainer button:hover {
background-color: #2980b9;
}

.formContainer button.submitting {
background-color: #95a5a6;
cursor: not-allowed;
}

.successMessage {
text-align: center;
padding: 20px;
}

.successMessage h2 {
color: #27ae60;
}

Advanced Features

Form Default Values

You can provide default values for form fields:

jsx
const { register } = useForm({
defaultValues: {
firstName: "John",
lastName: "Doe",
email: "[email protected]"
}
});

Form Reset

Reset the form to its default values:

jsx
const { reset } = useForm();

// Example: Reset form after submission
const onSubmit = (data) => {
console.log(data);
// Process form submission
reset(); // Reset form to default values
};

Form Watch

Watch specific form values to react to changes:

jsx
const { watch } = useForm();
const watchedValue = watch("fieldName");

// Now you can use watchedValue in your component
useEffect(() => {
if (watchedValue === "specific value") {
// Do something
}
}, [watchedValue]);

Form Context

For complex forms spread across multiple components, you can use FormProvider:

jsx
import { useForm, FormProvider } from 'react-hook-form';
import NestedComponent from './NestedComponent';

function ParentComponent() {
const methods = useForm();

const onSubmit = (data) => console.log(data);

return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{/* Your form fields */}
<NestedComponent />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
}

// In NestedComponent.js
import { useFormContext } from 'react-hook-form';

function NestedComponent() {
const { register, formState: { errors } } = useFormContext();

return (
<div>
<input {...register("nestedField", { required: true })} />
{errors.nestedField && <span>This field is required</span>}
</div>
);
}

Integration with Next.js API Routes

To submit form data to a Next.js API route:

jsx
const onSubmit = async (data) => {
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

const result = await response.json();

if (!response.ok) {
throw new Error(result.message || 'Something went wrong');
}

// Handle success (e.g., redirect, show success message)
console.log('Registration successful:', result);
} catch (error) {
// Handle error
console.error('Registration failed:', error);
}
};

And in your API route (pages/api/register.js):

jsx
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}

try {
const { email, password, firstName, lastName } = req.body;

// Validate data
if (!email || !password || !firstName || !lastName) {
return res.status(400).json({ message: 'Missing required fields' });
}

// Handle registration (e.g., save to database)
// This is where you would typically use a database adapter

// Return success
return res.status(201).json({
message: 'User registered successfully',
user: { email, firstName, lastName }
});
} catch (error) {
console.error('Registration error:', error);
return res.status(500).json({ message: 'Internal server error' });
}
}

Best Practices

  1. Separate form logic from UI components: Keep your form logic clean by separating it from UI components when forms become complex.

  2. Use form schemas for validation: Consider using libraries like Zod or Yup with React Hook Form for schema-based validation.

    jsx
    import { useForm } from 'react-hook-form';
    import { zodResolver } from '@hookform/resolvers/zod';
    import * as z from 'zod';

    const schema = z.object({
    name: z.string().min(1, "Name is required"),
    email: z.string().email("Invalid email address"),
    age: z.number().min(18, "Must be at least 18 years old")
    });

    function MyForm() {
    const { register, handleSubmit } = useForm({
    resolver: zodResolver(schema)
    });

    // Rest of your component
    }
  3. Handle form errors gracefully: Display validation errors clearly and provide helpful guidance.

  4. Consider accessibility: Ensure your forms are accessible by using proper labels, aria attributes, and keyboard navigation.

  5. Test your forms: Write tests for form validations and submissions.

Summary

React Hook Form is a powerful tool for handling forms in Next.js applications. It provides:

  • Simplified form state management
  • Built-in validation with clear error handling
  • Improved performance through minimal re-renders
  • Advanced features like form watching and context providers

By following the examples and practices outlined in this guide, you can create user-friendly, performant forms in your Next.js applications.

Additional Resources

Exercises

  1. Create a multi-step form using React Hook Form
  2. Build a dynamic form where fields appear/disappear based on other field values
  3. Implement a form with file uploads
  4. Create a form with custom validation rules
  5. Build a form that saves progress to localStorage


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)