React Hook Form
Forms are an essential part of many web applications, but building them from scratch in React can be tedious. React Hook Form is a powerful library that simplifies form handling with minimal code while maintaining excellent performance.
Introduction to React Hook Form
React Hook Form is a form management library for React that focuses on:
- Performance: Minimizes re-renders and optimizes validation
- Developer experience: Intuitive API with minimal boilerplate code
- Validation: Easy integration with schema validation libraries
- Flexibility: Works with uncontrolled components by default
Unlike other form libraries that use controlled components (which can lead to unnecessary re-renders), React Hook Form primarily leverages uncontrolled components and the browser's native form validation API, resulting in better performance.
Getting Started
Installation
To get started with React Hook Form, you need to install it in your React project:
npm install react-hook-form
# or
yarn add react-hook-form
Basic Usage
Here's a simple example of a login form with React Hook Form:
import { useForm } from "react-hook-form";
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = data => {
console.log(data);
// Handle form submission (e.g., API call)
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<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>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
{...register("password", {
required: "Password is required",
minLength: {
value: 8,
message: "Password must have at least 8 characters"
}
})}
/>
{errors.password && <p className="error">{errors.password.message}</p>}
</div>
<button type="submit">Login</button>
</form>
);
}
Key Concepts
The useForm
Hook
The useForm
hook is the primary way to create a form in React Hook Form. It returns several methods and states for managing your form:
const {
register, // Registers input with validation
handleSubmit, // Handles form submission
watch, // Watches input values
formState, // Contains form state like errors, isSubmitting
reset, // Resets the form
setValue, // Programmatically set values
getValues, // Get current values
control, // For custom inputs/components
// ... other methods
} = useForm();
You can also provide default values and form configuration:
const methods = useForm({
defaultValues: {
firstName: 'John',
lastName: 'Doe',
email: '[email protected]'
},
mode: 'onBlur', // When to trigger validation
});
Registering Fields with register
The register
function connects your input fields to the form:
<input {...register("firstName", { required: true })} />
The first parameter is the field name, and the second parameter is a validation object. Common validation rules include:
required
: Makes the field mandatoryminLength
/maxLength
: Controls the length of text inputsmin
/max
: Controls the value of numeric inputspattern
: Validates against a regular expressionvalidate
: Custom validation function
Form Submission
The handleSubmit
function processes the form data when submitted:
const onSubmit = data => {
console.log(data);
// Process form data
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Form fields */}
</form>
);
The onSubmit
callback only runs when the form is valid according to your validation rules.
Validation
Built-in Validation
React Hook Form provides several built-in validation rules:
<input
{...register("username", {
required: "Username is required",
minLength: {
value: 3,
message: "Username must be at least 3 characters"
},
maxLength: {
value: 20,
message: "Username cannot exceed 20 characters"
},
pattern: {
value: /^[A-Za-z0-9]+$/,
message: "Username can only contain letters and numbers"
}
})}
/>
Custom Validation
For more complex validations, you can use the validate
property:
<input
type="password"
{...register("confirmPassword", {
validate: value => {
const { password } = getValues();
return value === password || "Passwords don't match";
}
})}
/>
Schema Validation
React Hook Form works well with schema validation libraries like Yup, Zod, or Joi. Here's an example with Yup:
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
const schema = yup.object({
name: yup.string().required("Name is required"),
age: yup
.number()
.positive("Age must be a positive number")
.integer("Age must be an integer")
.required("Age is required"),
email: yup
.string()
.email("Please enter a valid email")
.required("Email is required"),
}).required();
function ProfileForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: yupResolver(schema)
});
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register("name")} placeholder="Name" />
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<input {...register("age")} placeholder="Age" type="number" />
{errors.age && <p>{errors.age.message}</p>}
</div>
<div>
<input {...register("email")} placeholder="Email" />
{errors.email && <p>{errors.email.message}</p>}
</div>
<button type="submit">Submit</button>
</form>
);
}
Advanced Features
Watch Fields
You can observe changes to specific form fields using the watch
function:
function WatchExample() {
const { register, watch } = useForm();
const watchedValue = watch("example"); // Watch a specific field
const allValues = watch(); // Watch all fields
return (
<form>
<input {...register("example")} />
<p>Current value: {watchedValue}</p>
</form>
);
}
Form Reset
Reset the form to its initial values:
function ResetExample() {
const { register, handleSubmit, reset } = useForm({
defaultValues: {
firstName: "John",
lastName: "Doe"
}
});
const onSubmit = data => {
console.log(data);
// After submission, reset the form
reset();
// You can also reset to specific values
// reset({ firstName: "Jane", lastName: "Smith" });
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<button type="submit">Submit</button>
<button type="button" onClick={() => reset()}>Reset</button>
</form>
);
}
Working with Custom Components
For custom components or third-party UI libraries, use the Controller
component:
import { useForm, Controller } from "react-hook-form";
import Select from "react-select";
import DatePicker from "react-datepicker";
function CustomComponentForm() {
const { control, handleSubmit } = useForm();
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="reactSelect"
render={({ field }) => (
<Select
{...field}
options={[
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" }
]}
/>
)}
/>
<Controller
control={control}
name="datePicker"
render={({ field }) => (
<DatePicker
selected={field.value}
onChange={(date) => field.onChange(date)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
}
Form Arrays
Handle dynamic fields like to-do lists or multiple address forms:
import { useForm, useFieldArray } from "react-hook-form";
function DynamicForm() {
const { register, control, handleSubmit } = useForm({
defaultValues: {
items: [{ name: "" }]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: "items"
});
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2>Items</h2>
{fields.map((field, index) => (
<div key={field.id}>
<input
{...register(`items.${index}.name`, { required: true })}
placeholder="Item name"
/>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => append({ name: "" })}
>
Add Item
</button>
<button type="submit">Submit</button>
</form>
);
}
Real-world Example: Registration Form
Let's build a complete registration form with React Hook Form, including validation, custom components, and error handling:
import { useForm, Controller } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useState } from "react";
// Schema for validation
const schema = yup.object({
firstName: yup.string().required("First name is required"),
lastName: yup.string().required("Last name is required"),
email: yup
.string()
.email("Please enter a valid email")
.required("Email is required"),
password: yup
.string()
.min(8, "Password must be at least 8 characters")
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
"Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character"
)
.required("Password is required"),
confirmPassword: yup
.string()
.oneOf([yup.ref("password"), null], "Passwords must match")
.required("Please confirm your password"),
dateOfBirth: yup
.date()
.max(new Date(), "Date of birth cannot be in the future")
.required("Date of birth is required"),
termsAccepted: yup
.boolean()
.oneOf([true], "You must accept the terms and conditions")
}).required();
function RegistrationForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
const {
register,
handleSubmit,
control,
formState: { errors },
reset
} = useForm({
resolver: yupResolver(schema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
password: "",
confirmPassword: "",
dateOfBirth: null,
termsAccepted: false
}
});
const onSubmit = async (data) => {
setIsSubmitting(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
console.log("Form submitted:", data);
setSubmitSuccess(true);
reset();
} catch (error) {
console.error("Error submitting form:", error);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="registration-container">
<h2>Create Account</h2>
{submitSuccess && (
<div className="success-message">
Registration successful! Please check your email to verify your account.
</div>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-row">
<div className="form-group">
<label htmlFor="firstName">First Name</label>
<input
id="firstName"
type="text"
{...register("firstName")}
className={errors.firstName ? "error-input" : ""}
/>
{errors.firstName && <p className="error-text">{errors.firstName.message}</p>}
</div>
<div className="form-group">
<label htmlFor="lastName">Last Name</label>
<input
id="lastName"
type="text"
{...register("lastName")}
className={errors.lastName ? "error-input" : ""}
/>
{errors.lastName && <p className="error-text">{errors.lastName.message}</p>}
</div>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register("email")}
className={errors.email ? "error-input" : ""}
/>
{errors.email && <p className="error-text">{errors.email.message}</p>}
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
{...register("password")}
className={errors.password ? "error-input" : ""}
/>
{errors.password && <p className="error-text">{errors.password.message}</p>}
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
{...register("confirmPassword")}
className={errors.confirmPassword ? "error-input" : ""}
/>
{errors.confirmPassword && <p className="error-text">{errors.confirmPassword.message}</p>}
</div>
<div className="form-group">
<label htmlFor="dateOfBirth">Date of Birth</label>
<Controller
control={control}
name="dateOfBirth"
render={({ field }) => (
<input
id="dateOfBirth"
type="date"
onChange={(e) => field.onChange(new Date(e.target.value))}
className={errors.dateOfBirth ? "error-input" : ""}
/>
)}
/>
{errors.dateOfBirth && <p className="error-text">{errors.dateOfBirth.message}</p>}
</div>
<div className="form-group checkbox">
<input
id="termsAccepted"
type="checkbox"
{...register("termsAccepted")}
/>
<label htmlFor="termsAccepted">
I agree to the Terms and Conditions
</label>
{errors.termsAccepted && <p className="error-text">{errors.termsAccepted.message}</p>}
</div>
<button
type="submit"
className="submit-button"
disabled={isSubmitting}
>
{isSubmitting ? "Registering..." : "Register"}
</button>
</form>
</div>
);
}
This example demonstrates a comprehensive registration form with:
- Field validation using Yup
- Password strength requirements
- Date input handling
- Form submission with loading state
- Success message after submission
- Custom styling for errors
Performance Benefits
React Hook Form's performance advantages can be visualized as follows:
Summary
React Hook Form offers a powerful, efficient way to handle forms in React applications. Its key advantages include:
- Performance optimization through uncontrolled components
- Simple but powerful API with minimal boilerplate
- Flexible validation options including built-in rules and schema validation
- Easy integration with custom components and UI libraries
- Advanced features like form arrays for dynamic inputs
By leveraging React Hook Form, you can create complex forms with less code and better performance compared to traditional controlled form approaches.
Additional Resources
To further enhance your knowledge of React Hook Form:
- Official documentation: React Hook Form
- GitHub repository: react-hook-form
- Example collection: React Hook Form examples
Practice Exercises
- Create a multi-step form wizard using React Hook Form
- Build a dynamic form with add/remove capabilities for a shopping list
- Implement a form with conditional fields that appear based on other field values
- Create a form with file uploads and preview functionality
- Build a form with real-time validation and password strength indicator
By practicing these exercises, you'll gain a deeper understanding of React Hook Form's capabilities and how to implement them in real-world scenarios.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)