
React Hook Form: Fewer Renders, Fewer Bugs
Controlled inputs re-render the form on every keystroke and give you nothing for it. Uncontrolled forms with a validation schema are faster and shorter at the same time.
The default way to build a React form makes every keystroke a state update, and every state update a re-render of the form and its children. On a login box nobody notices. On a twenty-field application form on a mid-range Android phone, people notice.
The uncontrolled approach hands input state back to the DOM, which was always good at it.
The shape of it
import { useForm } from 'react-hook-form'
function Contact() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm()
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<label htmlFor="email">Email</label>
<input id="email" {...register('email', { required: 'Email is required' })}
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined} />
{errors.email && <p id="email-error" role="alert">{errors.email.message}</p>}
<button disabled={isSubmitting}>Send</button>
</form>
)
}
There is no useState per field, no onChange handler, and no re-render while typing. The component renders when validation state changes — which is when something visible actually changed.
Validate once, on both sides
Hand-written validation rules drift from the server's rules within about two sprints. Define the shape once and use it in both places:
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
const schema = z.object({
email: z.string().email('That email does not look right'),
message: z.string().min(20, 'Give me a little more detail'),
})
const { register, handleSubmit } = useForm({ resolver: zodResolver(schema) })
The same schema runs on the API route. The browser check is a courtesy; the server check is the real one. When the rule changes, it changes in one file and neither side can fall behind.
The accessibility part is not optional
Notice aria-invalid, aria-describedby and role="alert" above. A form that shows a red border and nothing else is broken for anyone using a screen reader — and the error you are already rendering is one attribute away from being announced properly. This is the cheapest accessibility win in frontend work, and it is skipped in most codebases I review.
Keep noValidate on the form so your messages win over the browser's, which cannot be styled, translated or made bilingual.
Where controlled still wins
Anything that reacts as you type — a live search, a character counter, a preview pane — needs the value in React. Use watch for a specific field rather than making the whole form controlled to get one live value.
Why it is on every project I ship
Forms are where conversion is won or lost, and they are also where the accessibility audits fail. A library that makes the fast path and the accessible path the same path is doing the most useful thing a library can do.
Resources
- Repo: react-hook-form/react-hook-form
- Docs: react-hook-form.com
- Video walkthroughs: YouTube: react hook form zod tutorial
- Related: Zod: schemas as the single source of truth
Need this built properly?
I build secure, fast, bilingual platforms for clients across Egypt, Saudi Arabia, the UAE and Kuwait.


