React Hook Form
IntermediateThe library nearly every React job uses for forms. It keeps inputs uncontrolled for speed, wires validation to a schema, and removes the boilerplate you would otherwise write for every field.
Overview
Hand-rolling a form works for two fields. At ten, with per-field touched state, error state, submit state and validation timing, it becomes a large amount of repetitive code that is easy to get subtly wrong. React Hook Form registers each input directly with the DOM, so typing does not re-render the form, and exposes exactly the state a form needs. Knowing it is close to expected for a React role, and the API is small enough to learn in an afternoon.
The Basic Form
register, handleSubmit, and formState — that is most of the library.
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isDirty, isValid },
reset,
setError,
} = useForm({
resolver: zodResolver(SignupSchema), // the schema from the last concept
defaultValues: { name: '', email: '' },
mode: 'onBlur', // the timing convention
})
async function onSubmit(values) { // only runs if validation passed
await signup(values)
reset()
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} aria-invalid={!!errors.email} />
{errors.email && <p role="alert">{errors.email.message}</p>}
<button disabled={isSubmitting || !isDirty}>
{isSubmitting ? 'Creating…' : 'Create account'}
</button>
</form>
)
}
// Typing re-renders nothing. Only error state changes cause renders.Server Errors and Async Checks
Bringing backend failures into the same error display.
async function onSubmit(values) {
try {
await signup(values)
} catch (err) {
if (err.status === 422) {
for (const [field, message] of Object.entries(err.fieldErrors)) {
setError(field, { type: 'server', message }) // same UI as client errors
}
return
}
setError('root', { message: 'Something went wrong. Please try again.' })
}
}
{errors.root && <p role="alert">{errors.root.message}</p>}
// Async validation for uniqueness, debounced by the resolver's timing
email: z.string().email().refine(
async (v) => !(await emailTaken(v)),
'That email is already registered',
)
// Watching a field to drive conditional UI
const plan = watch('plan')
{plan === 'team' && <input {...register('seats')} />}
// watch re-renders on change — use it deliberately, not everywhere.Custom and Third-Party Inputs
Controller is how a component that does not expose a DOM ref joins in.
import { Controller } from 'react-hook-form'
// A date picker, a rich select, an editor — anything with its own
// value/onChange API rather than a plain input
<Controller
name="deadline"
control={control}
render={({ field, fieldState }) => (
<DatePicker
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
error={fieldState.error?.message}
/>
)}
/>
// Dynamic lists of fields
const { fields, append, remove } = useFieldArray({ control, name: 'items' })
{fields.map((f, i) => (
<div key={f.id}> {/* f.id, not the index */}
<input {...register(`items.${i}.title`)} />
<button type="button" onClick={() => remove(i)}>Remove</button>
</div>
))}
// type="button" matters — inside a form, a bare <button> submits.Key Points to Remember
- 1register wires an input straight to the DOM, so typing does not re-render the form
- 2handleSubmit runs validation first and only calls your handler with valid values
- 3formState gives errors, isSubmitting, isDirty and isValid — the flags you would otherwise track by hand
- 4setError puts server failures into the same display as client validation errors
- 5Controller adapts components that expose their own value/onChange instead of a DOM ref
Interview Questions
Sign in to ask AriaHow does React Hook Form avoid re-rendering on every keystroke?
How do you display a server-side validation error next to the right field?
When do you need Controller instead of register?
Ask Aria about React Hook Form
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.