React.js Fundamentals

Forms with React Hook Form

18 min Lesson 25 of 40

Forms with React Hook Form

React Hook Form is a performant, flexible library for building forms in React. It reduces re-renders, simplifies validation, and provides excellent TypeScript support. It's built on uncontrolled components for better performance while maintaining a great developer experience.

Why React Hook Form?

Key Benefits:
  • Minimal re-renders - only re-renders fields that need updates
  • Simple API with register() for field registration
  • Built-in validation with HTML5 and custom rules
  • Easy integration with UI libraries (Material-UI, Ant Design)
  • Works with Zod, Yup, Joi for schema validation
  • Small bundle size (~9KB gzipped)

Basic Setup and Usage

Install and create your first form:

// Install npm install react-hook-form // Basic form import { useForm } from 'react-hook-form'; function LoginForm() { const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm(); const onSubmit = async (data) => { console.log(data); // { email: "...", password: "..." } // Simulate API call await new Promise(resolve => setTimeout(resolve, 2000)); alert('Login successful!'); }; 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 && ( <span className="error">{errors.email.message}</span> )} </div> <div> <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 && ( <span className="error">{errors.password.message}</span> )} </div> <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Logging in...' : 'Login'} </button> </form> ); }
Register Spread Operator: The {...register('fieldName')} syntax spreads ref, onChange, onBlur, and name props to your input. This connects your input to React Hook Form's internal state management without controlled components.

Advanced Validation Rules

React Hook Form supports comprehensive validation rules:

// Registration form with complex validation function RegisterForm() { const { register, handleSubmit, watch, formState: { errors } } = useForm({ mode: 'onBlur', // Validate on blur defaultValues: { username: '', email: '', password: '', confirmPassword: '', age: '', terms: false } }); // Watch password for confirm validation const password = watch('password'); const onSubmit = (data) => { console.log('Form data:', data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> {/* Username */} <div> <label>Username</label> <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, numbers, and underscores' } })} /> {errors.username && <span>{errors.username.message}</span>} </div> {/* Email */} <div> <label>Email</label> <input type="email" {...register('email', { required: 'Email is required', validate: { // Custom async validation checkDomain: async (value) => { const domain = value.split('@')[1]; if (domain === 'tempmail.com') { return 'Temporary email addresses are not allowed'; } return true; }, // Multiple validators noSpaces: (value) => !value.includes(' ') || 'Email cannot contain spaces' } })} /> {errors.email && <span>{errors.email.message}</span>} </div> {/* Password */} <div> <label>Password</label> <input type="password" {...register('password', { required: 'Password is required', validate: { hasUpperCase: (value) => /[A-Z]/.test(value) || 'Password must contain uppercase letter', hasLowerCase: (value) => /[a-z]/.test(value) || 'Password must contain lowercase letter', hasNumber: (value) => /[0-9]/.test(value) || 'Password must contain number', hasSpecial: (value) => /[!@#$%^&*]/.test(value) || 'Password must contain special character' } })} /> {errors.password && <span>{errors.password.message}</span>} </div> {/* Confirm Password */} <div> <label>Confirm Password</label> <input type="password" {...register('confirmPassword', { required: 'Please confirm password', validate: (value) => value === password || 'Passwords do not match' })} /> {errors.confirmPassword && ( <span>{errors.confirmPassword.message}</span> )} </div> {/* Age */} <div> <label>Age</label> <input type="number" {...register('age', { required: 'Age is required', min: { value: 18, message: 'You must be at least 18 years old' }, max: { value: 120, message: 'Please enter a valid age' }, valueAsNumber: true // Convert to number })} /> {errors.age && <span>{errors.age.message}</span>} </div> {/* Terms Checkbox */} <div> <label> <input type="checkbox" {...register('terms', { required: 'You must accept the terms and conditions' })} /> I accept the terms and conditions </label> {errors.terms && <span>{errors.terms.message}</span>} </div> <button type="submit">Register</button> </form> ); }

Schema Validation with Zod

For complex forms, use schema validation libraries like Zod:

// Install npm install zod @hookform/resolvers // Schema validation with Zod import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; // Define schema const userSchema = z.object({ firstName: z.string() .min(2, 'First name must be at least 2 characters') .max(50, 'First name cannot exceed 50 characters'), lastName: z.string() .min(2, 'Last name must be at least 2 characters') .max(50, 'Last name cannot exceed 50 characters'), email: z.string() .email('Invalid email address') .toLowerCase(), phone: z.string() .regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number') .optional(), age: z.number() .int() .min(18, 'You must be at least 18') .max(120, 'Please enter a valid age'), website: z.string() .url('Invalid URL') .optional() .or(z.literal('')), role: z.enum(['user', 'admin', 'moderator'], { errorMap: () => ({ message: 'Please select a valid role' }) }), bio: z.string() .max(500, 'Bio cannot exceed 500 characters') .optional(), terms: z.boolean() .refine(val => val === true, { message: 'You must accept the terms' }) }); type UserFormData = z.infer<typeof userSchema>; function UserForm() { const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<UserFormData>({ resolver: zodResolver(userSchema), defaultValues: { firstName: '', lastName: '', email: '', age: 18, role: 'user', terms: false } }); const onSubmit = async (data: UserFormData) => { console.log('Valid data:', data); // Data is fully typed and validated }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('firstName')} placeholder="First Name" /> {errors.firstName && <span>{errors.firstName.message}</span>} <input {...register('lastName')} placeholder="Last Name" /> {errors.lastName && <span>{errors.lastName.message}</span>} <input {...register('email')} type="email" placeholder="Email" /> {errors.email && <span>{errors.email.message}</span>} <input {...register('age', { valueAsNumber: true })} type="number" placeholder="Age" /> {errors.age && <span>{errors.age.message}</span>} <select {...register('role')}> <option value="user">User</option> <option value="admin">Admin</option> <option value="moderator">Moderator</option> </select> {errors.role && <span>{errors.role.message}</span>} <label> <input type="checkbox" {...register('terms')} /> Accept Terms </label> {errors.terms && <span>{errors.terms.message}</span>} <button type="submit" disabled={isSubmitting}> Submit </button> </form> ); }
Performance Note: React Hook Form is uncontrolled by default, which means it doesn't re-render on every keystroke. Only use watch() when you need real-time values, as it causes re-renders. For validation feedback, rely on errors instead.

Dynamic Fields and Field Arrays

Handle dynamic form fields like adding/removing items:

// Dynamic form fields import { useForm, useFieldArray } from 'react-hook-form'; function ContactForm() { const { register, control, handleSubmit } = useForm({ defaultValues: { name: '', phoneNumbers: [{ number: '' }] } }); const { fields, append, remove } = useFieldArray({ control, name: 'phoneNumbers' }); const onSubmit = (data) => { console.log(data); // { name: "...", phoneNumbers: [{ number: "..." }, ...] } }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('name', { required: true })} placeholder="Name" /> <h3>Phone Numbers</h3> {fields.map((field, index) => ( <div key={field.id}> <input {...register(`phoneNumbers.${index}.number`, { required: 'Phone number is required' })} placeholder={`Phone #${index + 1}`} /> {index > 0 && ( <button type="button" onClick={() => remove(index)} > Remove </button> )} </div> ))} <button type="button" onClick={() => append({ number: '' })} > Add Phone Number </button> <button type="submit">Submit</button> </form> ); }

Controlled Components Integration

Use Controller for third-party controlled components:

// Integrating with Material-UI or other controlled components import { useForm, Controller } from 'react-hook-form'; import Select from 'react-select'; // Example third-party component function ControlledForm() { const { control, handleSubmit } = useForm({ defaultValues: { country: null, tags: [] } }); const onSubmit = (data) => console.log(data); const countryOptions = [ { value: 'us', label: 'United States' }, { value: 'uk', label: 'United Kingdom' }, { value: 'ca', label: 'Canada' } ]; return ( <form onSubmit={handleSubmit(onSubmit)}> <Controller name="country" control={control} rules={{ required: 'Country is required' }} render={({ field, fieldState: { error } }) => ( <div> <Select {...field} options={countryOptions} placeholder="Select country" /> {error && <span>{error.message}</span>} </div> )} /> <Controller name="tags" control={control} render={({ field }) => ( <Select {...field} isMulti options={[ { value: 'react', label: 'React' }, { value: 'vue', label: 'Vue' }, { value: 'angular', label: 'Angular' } ]} placeholder="Select tags" /> )} /> <button type="submit">Submit</button> </form> ); }

Practice Exercise 1: Multi-Step Survey Form

Build a 3-step survey form:

  1. Step 1: Personal info (name, email, age) with validation
  2. Step 2: Preferences (interests as checkboxes, country select)
  3. Step 3: Review all data before submission
  4. Use watch() to show progress bar based on filled fields
  5. Persist form state in localStorage between steps

Practice Exercise 2: Invoice Creator

Create a dynamic invoice form:

  1. Client details section (name, email, address)
  2. Dynamic line items array (description, quantity, price)
  3. Auto-calculate subtotal, tax (8%), and total
  4. Add/remove line items with useFieldArray
  5. Validate: minimum 1 item, all prices > 0

Practice Exercise 3: Job Application Form with File Upload

Build a comprehensive job application:

  1. Personal details with Zod schema validation
  2. Experience section: dynamic array of jobs (company, role, dates)
  3. Skills: multi-select with Controller
  4. Resume upload with file validation (PDF only, max 5MB)
  5. Cover letter textarea with character counter (max 1000 chars)

Summary

React Hook Form provides a powerful yet simple API for form management. Use register() for basic inputs, schema validation with Zod for complex forms, useFieldArray for dynamic fields, and Controller for third-party components. Its uncontrolled approach ensures excellent performance even with large forms.