React Hook Form is a library that manages form state through refs instead of per-keystroke state updates, so fields stay uncontrolled and the form re-renders far less. You register each input, attach a submit handler, and read validation errors from a formState object. This tutorial sets it up with built-in rules and a Zod schema.
Install and wire up useForm
The library has no runtime dependencies and supports React 16.8 through 19. Install it, then call the useForm hook and pull out register and handleSubmit.
npm install react-hook-formThe register function attaches a field by name and returns the props to spread onto the input. The handleSubmit function wraps your onSubmit callback so validation runs first. Spreading the returned props wires the ref, the change handler, and the name in one step, which is why a React Hook Form input never needs a value or onChange prop.
import { useForm } from "react-hook-form";
export default function SignupForm() {
const { register, handleSubmit } = useForm();
function onSubmit(data) {
alert(JSON.stringify(data));
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="name">Name</label>
<input id="name" {...register("name")} />
<button type="submit">Submit</button>
</form>
);
}Type a name and press Submit. handleSubmit collects the field into an object and passes it to onSubmit, so the alert shows the submitted values. The input never needed a value or onChange prop because the library reads it from the DOM.
Add validation rules and show errors
Register accepts a second argument of validation rules that mirror the HTML standard: required, minLength, maxLength, pattern, min, max, and a custom validate function. Passing a string to a rule turns it into the error message.
import { useForm } from "react-hook-form";
export default function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
function onSubmit(data) {
alert(JSON.stringify(data));
}
// the return below continues this same componentDestructure errors from formState, and the rule message you pass to register becomes errors.name.message for the field that failed validation.
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="name">Name</label>
<input id="name" {...register("name", { required: "Enter your name." })} />
{errors.name && <p role="alert">{errors.name.message}</p>}
<button type="submit">Submit</button>
</form>
);
}Submit the empty form and handleSubmit blocks onSubmit, then fills errors with the field failure. The paragraph renders the message from errors.name.message, and role alert announces it to screen readers. By default validation runs on submit, so users are not interrupted while typing.
Set default values and validation timing
useForm accepts a defaultValues option that seeds every field before the user types, and a mode option that decides when validation runs. The default mode is onSubmit, which checks the whole form once when the user submits. Set mode to onBlur to validate each field as the user leaves it, or onChange to validate while typing.
The onChange mode feels immediate but re-renders more, so save it for small forms or fields where live feedback is the point. After a failed submit, reValidateMode controls when a field with an error is rechecked, and it defaults to onChange.
These options only change timing, not the rules themselves, so the validation you wrote earlier behaves the same in every mode. Start with the default and adjust only when a specific field needs earlier feedback.
Add a schema with Zod resolver
For many rules or shared schemas, swap the built-in rules for a schema library. Install the resolvers package and Zod, then define the schema once.
npm install @hookform/resolvers zodThe resolver package bridges Zod to React Hook Form. Next, define the schema with Zod's object and string helpers below.
import { z } from "zod";
const schema = z.object({
email: z.string().email("Enter a valid email address."),
});Pass the schema to useForm through the resolver option, and errors.email.message carries Zod's message exactly like the built-in rules. The full resolver setup and more schema patterns live in how to validate React forms with Zod.
Choose the library over hand-written forms
Reach for the library when a form has many fields, complex rules, or performance-sensitive lists. It removes the boilerplate of values and errors state while keeping the form fast.
- Many fields with repeated validation rules.
- Forms inside lists that should not re-render on every keystroke.
- Schema validation you want to share between client and server.
For small forms, plain state is often simpler, and React form validation without a library covers that hand-written path. For a full comparison against another popular library, see React Hook Form vs Formik.
Rune AI
Key Insights
- Install react-hook-form and call useForm.
- Spread register into each input to connect it.
- Pass rules to register for built-in validation.
- Read messages from formState.errors.
- Use a resolver with Zod for schema validation.
Frequently Asked Questions
Is React Hook Form controlled or uncontrolled?
How do I show a validation error in React Hook Form?
Does React Hook Form work with React 19?
Conclusion
React Hook Form registers fields with refs, validates on submit, and reports errors through formState. Add rules to register, then surface each message accessibly.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.