React Hook Form Overview
Managing forms manually — tracking field values, touched state, error messages, and validation timing — generates a lot of boilerplate. React Hook Form (RHF) is a lightweight library that eliminates most of that code while delivering better performance than fully controlled forms.
Why React Hook Form?
Fewer re-renders — inputs are uncontrolled by default; the form only re-renders on submit and explicit
watch()callsLess code — no manual
useStatefor field values, noonChangehandlers, notouchedtrackingBuilt-in validation — required, minLength, maxLength, pattern, validate (custom function), and seamless schema integration (Zod, Yup)
TypeScript-first — generic
useForm<T>()types your form values automaticallySmall bundle — ~9 KB gzipped, zero dependencies
Installation
npm install react-hook-form
The Core API
Everything starts with the useForm() hook, which returns a set of helpers:
import { useForm } from 'react-hook-form'
const {
register, // connects an input to RHF
handleSubmit, // wraps your submit function, handles preventDefault
formState: { errors, isSubmitting, isValid },
watch, // subscribe to a field's live value
reset, // reset form to default values
setValue, // programmatically set a field value
getValues, // read current values without subscribing
} = useForm({
defaultValues: {
firstName : '',
email : '',
password : '',
},
mode: 'onBlur', // when to trigger validation: onChange | onBlur | onSubmit (default)
})register()
register(name, options) returns an object with ref, name, onChange, and onBlur — spread it onto any input to connect it to RHF:
<input
type="email"
placeholder="Email"
{...register('email', {
required : 'Email is required.',
pattern : {
value : /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message : 'Enter a valid email address.',
},
})}
/>handleSubmit()
Wrap your submit handler in handleSubmit. RHF will run all validation first and only call your function if the form is valid:
function onSubmit(data) {
// 'data' is a fully typed object with all field values
console.log(data)
// { firstName: 'Alice', email: 'alice@example.com', password: 'Secret1!' }
}
<form onSubmit={handleSubmit(onSubmit)}>formState.errors
When validation fails, errors is populated with objects containing a message string from your validation rules:
{errors.email && (
<span role="alert" style={{ color: 'red' }}>
{errors.email.message}
</span>
)}A Complete Form with React Hook Form
import { useForm } from 'react-hook-form'
function SignupForm() {
const {
register,
handleSubmit,
watch,
formState: { errors, isSubmitting },
} = useForm({
defaultValues: {
firstName : '',
lastName : '',
email : '',
password : '',
confirmPassword : '',
role : 'viewer',
newsletter : false,
},
mode: 'onBlur',
})
// Watch password field to cross-validate confirmPassword
const password = watch('password')
async function onSubmit(data) {
// Simulate async API call
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('Registered:', data)
alert(`Welcome, ${data.firstName}!`)
}
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate style={{ display: 'grid', gap: 16 }}>
{/* First Name */}
<div>
<label>First name *</label>
<input
{...register('firstName', {
required : 'First name is required.',
minLength : { value: 2, message: 'At least 2 characters.' },
})}
/>
{errors.firstName && <span role="alert">{errors.firstName.message}</span>}
</div>
{/* Last Name */}
<div>
<label>Last name</label>
<input {...register('lastName')} />
</div>
{/* Email */}
<div>
<label>Email *</label>
<input
type="email"
{...register('email', {
required : 'Email is required.',
pattern : {
value : /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message : 'Enter a valid email address.',
},
})}
/>
{errors.email && <span role="alert">{errors.email.message}</span>}
</div>
{/* Password */}
<div>
<label>Password *</label>
<input
type="password"
{...register('password', {
required : 'Password is required.',
minLength : { value: 8, message: 'Minimum 8 characters.' },
pattern : {
value : /^(?=.*[A-Z])(?=.*[0-9])/,
message : 'Must contain at least one uppercase letter and one number.',
},
})}
/>
{errors.password && <span role="alert">{errors.password.message}</span>}
</div>
{/* Confirm Password */}
<div>
<label>Confirm password *</label>
<input
type="password"
{...register('confirmPassword', {
required : 'Please confirm your password.',
validate : value => value === password || 'Passwords do not match.',
})}
/>
{errors.confirmPassword && (
<span role="alert">{errors.confirmPassword.message}</span>
)}
</div>
{/* Role */}
<div>
<label>Role</label>
<select {...register('role')}>
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
</div>
{/* Newsletter */}
<label>
<input type="checkbox" {...register('newsletter')} />
{' Subscribe to newsletter'}
</label>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating account...' : 'Create Account'}
</button>
</form>
)
}Validation Rules
Rule | Type | Example |
|---|---|---|
required | boolean | string | required: 'Name is required.' |
minLength | { value, message } | minLength: { value: 3, message: 'Too short.' } |
maxLength | { value, message } | maxLength: { value: 50, message: 'Too long.' } |
min | { value, message } | min: { value: 18, message: 'Must be 18+.' } |
max | { value, message } | max: { value: 99, message: 'Must be under 99.' } |
pattern | { value: RegExp, message } | pattern: { value: /^[A-Z]+$/, message: 'Uppercase only.' } |
validate | (value) => true | string | validate: v => v !== 'admin' || 'Reserved name.' |
Integration with MUI (Material-UI)
RHF works with any component library. For MUI, use the Controller wrapper when a component does not expose a native HTML ref:
import { useForm, Controller } from 'react-hook-form'
import { TextField, Checkbox, FormControlLabel } from '@mui/material'
function MuiForm() {
const { control, handleSubmit, formState: { errors } } = useForm()
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<Controller
name="email"
control={control}
defaultValue=""
rules={{ required: 'Email is required.' }}
render={({ field }) => (
<TextField
{...field}
label="Email"
type="email"
error={!!errors.email}
helperText={errors.email?.message}
/>
)}
/>
<button type="submit">Submit</button>
</form>
)
}RHF vs Manual Controlled Forms
Aspect | Manual Controlled | React Hook Form |
|---|---|---|
Re-renders per keystroke | One per field | Zero (uncontrolled) |
Boilerplate | High (state + handlers + errors + touched) | Minimal |
Validation library | Custom or Zod (manual wire-up) | Built-in + Zod/Yup resolver |
Bundle size | Zero (built-in React) | ~9 KB gzipped |
TypeScript support | Manual typing | Generic useForm<T> |
Integration with UI libs | Native | Controller wrapper needed |