Get Started
Simple form validation with React Hook Form.
Quick start
React Hook Form manages form values, validation, errors, and submission while working with standard React inputs.
Install
npm install react-hook-form
Build your first form
This example registers an email field, validates it when the form is submitted, and passes valid data to onSubmit.
import { useForm, type SubmitHandler } from "react-hook-form"type FormValues = {email: string}export default function App() {const {register,handleSubmit,formState: { errors },} = useForm<FormValues>({defaultValues: { email: "" },})const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><label htmlFor="email">Email</label><inputid="email"type="email"aria-invalid={errors.email ? "true" : "false"}{...register("email", { required: "Email is required" })}/>{errors.email && <p role="alert">{errors.email.message}</p>}<button type="submit">Submit</button></form>)}
import { useForm } from "react-hook-form"export default function App() {const {register,handleSubmit,formState: { errors },} = useForm({defaultValues: { email: "" },})const onSubmit = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><label htmlFor="email">Email</label><inputid="email"type="email"aria-invalid={errors.email ? "true" : "false"}{...register("email", { required: "Email is required" })}/>{errors.email && <p role="alert">{errors.email.message}</p>}<button type="submit">Submit</button></form>)}
The example uses four fundamental pieces:
useFormcreates the form and its methods.registerconnects a native input to the form and attaches validation rules.handleSubmitvalidates the form before it calls your submit handler.formState.errorscontains the current validation errors.
Use defaultValues for the initial value of every field. This gives React Hook Form one consistent baseline for dirty state, resetting, and controlled components.
Choose your next step
| If you want to… | Continue with… |
|---|---|
| Validate native inputs | Register fields and Apply validation |
| Use Zod, Yup, or another schema library | Schema validation or integration guides |
| Connect shadcn/ui, MUI, React-Select, or AntD | Controlled inputs |
| Submit to an API or Server Action | Form component |
| Build with React Native | React Native |
| Add end-to-end TypeScript types | TypeScript |
React Web Video Tutorial
This video tutorial illustrates the basic usage and concepts of React Hook Form.
Register fields
One of the key concepts in React Hook Form is to register your component into the hook. This will make its value available for both the form validation and submission.
Note: Each field is required to have a name as a key for the registration process.
import { useForm, SubmitHandler } from "react-hook-form"enum GenderEnum {female = "female",male = "male",other = "other",}interface IFormInput {firstName: stringgender: GenderEnum}export default function App() {const { register, handleSubmit } = useForm<IFormInput>()const onSubmit: SubmitHandler<IFormInput> = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><label>First Name</label><input {...register("firstName")} /><label>Gender Selection</label><select {...register("gender")}><option value="female">female</option><option value="male">male</option><option value="other">other</option></select><input type="submit" /></form>)}
import { useForm } from "react-hook-form"export default function App() {const { register, handleSubmit } = useForm()const onSubmit = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><input {...register("firstName")} /><select {...register("gender")}><option value="female">female</option><option value="male">male</option><option value="other">other</option></select><input type="submit" /></form>)}
Apply validation
React Hook Form makes form validation easy by aligning with the existing HTML standard for form validation.
List of validation rules supported:
- required
- min
- max
- minLength
- maxLength
- pattern
- validate
You can read more details on each rule in the register section.
import { useForm, SubmitHandler } from "react-hook-form"interface IFormInput {firstName: stringlastName: stringage: number}export default function App() {const { register, handleSubmit } = useForm<IFormInput>()const onSubmit: SubmitHandler<IFormInput> = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><input {...register("firstName", { required: true, maxLength: 20 })} /><input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} /><input type="number" {...register("age", { min: 18, max: 99 })} /><input type="submit" /></form>)}
import { useForm } from "react-hook-form"export default function App() {const { register, handleSubmit } = useForm()const onSubmit = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><input {...register("firstName", { required: true, maxLength: 20 })} /><input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} /><input type="number" {...register("age", { min: 18, max: 99 })} /><input type="submit" /></form>)}
Integrating an existing form
Integrating an existing form is simple. The important step is to register the component's ref and assign the relevant props to your input.
import { forwardRef } from "react"import {FieldPath,useForm,UseFormRegister,SubmitHandler,} from "react-hook-form"interface IFormValues {"First Name": stringAge: number}type InputProps = {label: FieldPath<IFormValues>register: UseFormRegister<IFormValues>required: boolean}// The following component is an example of your existing Input Componentconst Input = ({ label, register, required }: InputProps) => (<><label>{label}</label><input {...register(label, { required })} /></>)// you can use forwardRef to pass the ref tooconst Select = forwardRef<HTMLSelectElement,{ label: string } & ReturnType<UseFormRegister<IFormValues>>>(({ onChange, onBlur, name, label }, ref) => (<><label>{label}</label><select name={name} ref={ref} onChange={onChange} onBlur={onBlur}><option value="20">20</option><option value="30">30</option></select></>))const App = () => {const { register, handleSubmit } = useForm<IFormValues>()const onSubmit: SubmitHandler<IFormValues> = (data) => {alert(JSON.stringify(data))}return (<form onSubmit={handleSubmit(onSubmit)}><Input label="First Name" register={register} required /><Select label="Age" {...register("Age")} /><input type="submit" /></form>)}
import { forwardRef } from "react"import { useForm } from "react-hook-form"// The following component is an example of your existing Input Componentconst Input = ({ label, register, required }) => (<><label>{label}</label><input {...register(label, { required })} /></>)// you can use forwardRef to pass the ref tooconst Select = forwardRef(({ onChange, onBlur, name, label }, ref) => (<><label>{label}</label><select name={name} ref={ref} onChange={onChange} onBlur={onBlur}><option value="20">20</option><option value="30">30</option></select></>))const App = () => {const { register, handleSubmit } = useForm()const onSubmit = (data) => {alert(JSON.stringify(data))}return (<form onSubmit={handleSubmit(onSubmit)}><Input label="First Name" register={register} required /><Select label="Age" {...register("Age")} /><input type="submit" /></form>)}
Integrating with UI libraries
React Hook Form has made it easy to integrate with external UI component libraries. If the component doesn't expose the input's ref, then you should use the Controller component, which will take care of the registration process.
import Select from "react-select"import { useForm, Controller, SubmitHandler } from "react-hook-form"import { Input } from "@mui/material"interface IFormInput {firstName: stringlastName: stringiceCreamType: { label: string; value: string }}const App = () => {const { control, handleSubmit } = useForm<IFormInput>({defaultValues: {firstName: "",lastName: "",iceCreamType: { label: "", value: "" },},})const onSubmit: SubmitHandler<IFormInput> = (data) => {console.log(data)}return (<form onSubmit={handleSubmit(onSubmit)}><Controllername="firstName"control={control}render={({ field }) => <Input {...field} />}/><Controllername="iceCreamType"control={control}render={({ field }) => (<Select{...field}options={[{ value: "chocolate", label: "Chocolate" },{ value: "strawberry", label: "Strawberry" },{ value: "vanilla", label: "Vanilla" },]}/>)}/><input type="submit" /></form>)}
import Select from "react-select"import { useForm, Controller } from "react-hook-form"import { Input } from "@mui/material"const App = () => {const { control, handleSubmit } = useForm({defaultValues: {firstName: "",select: {},},})const onSubmit = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><Controllername="firstName"control={control}render={({ field }) => <Input {...field} />}/><Controllername="select"control={control}render={({ field }) => (<Select{...field}options={[{ value: "chocolate", label: "Chocolate" },{ value: "strawberry", label: "Strawberry" },{ value: "vanilla", label: "Vanilla" },]}/>)}/><input type="submit" /></form>)}
Integrating Controlled Inputs
This library embraces uncontrolled components and native HTML inputs. However, it's hard to avoid working with external controlled components such as shadcn/ui, React-Select, AntD and MUI. To make this simple, we provide a wrapper component, Controller, to streamline the integration process while still giving you the freedom to use a custom register.
Using Component API
import { useForm, Controller, SubmitHandler } from "react-hook-form"import { Checkbox } from "@mui/material"interface IFormInputs {MyCheckbox: boolean}function App() {const { handleSubmit, control, reset } = useForm<IFormInputs>({defaultValues: {MyCheckbox: false,},})const onSubmit: SubmitHandler<IFormInputs> = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><Controllername="MyCheckbox"control={control}rules={{ required: true }}render={({ field }) => <Checkbox {...field} />}/><input type="submit" /></form>)}
import { useForm, Controller } from "react-hook-form"import { Checkbox } from "@mui/material"function App() {const { handleSubmit, control, reset } = useForm({defaultValues: {MyCheckbox: false,},})const onSubmit = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><Controllername="MyCheckbox"control={control}rules={{ required: true }}render={({ field }) => <Checkbox {...field} />}/><input type="submit" /></form>)}
import { zodResolver } from "@hookform/resolvers/zod"import { useForm } from "react-hook-form"import { z } from "zod"import { Form, FormControl, FormField } from "@/components/ui/form"import { Input } from "@/components/ui/input"const FormSchema = z.object({username: z.string().min(2, {message: "Username must be at least 2 characters.",}),})export function InputForm() {const form = useForm({resolver: zodResolver(FormSchema),defaultValues: {username: "",},})function onSubmit(data: z.output<typeof FormSchema>) {console.log(data)}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)}><FormFieldcontrol={form.control}name="username"render={({ field }) => (<FormControl><Input {...field} /></FormControl>)}/><button type="submit">Submit</button></form></Form>)}
Note: The shadcn/ui example above requires installing @hookform/resolvers and zod:
npm install @hookform/resolvers zod
If you hit Cannot find module '@hookform/resolvers/zod' (or zod/v4/core), it's almost always a version mismatch: Zod v4 support requires @hookform/resolvers ^3.10 or newer paired with zod ^3.25 or zod v4, and your tsconfig.json needs "moduleResolution": "bundler" (or "node16"/"nodenext") so the package's subpath exports resolve correctly. See react-hook-form/resolvers#839 for details.
Using Hooks API
import { useForm, useController, UseControllerProps } from "react-hook-form"type FormValues = {FirstName: string}function Input(props: UseControllerProps<FormValues>) {const { field, fieldState } = useController(props)return (<div><input {...field} placeholder={props.name} /><p>{fieldState.isTouched && "Touched"}</p><p>{fieldState.isDirty && "Dirty"}</p><p>{fieldState.invalid ? "invalid" : "valid"}</p></div>)}export default function App() {const { handleSubmit, control } = useForm<FormValues>({defaultValues: {FirstName: "",},mode: "onChange",})const onSubmit = (data: FormValues) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><Input control={control} name="FirstName" rules={{ required: true }} /><input type="submit" /></form>)}
import { TextField } from "@mui/material"import { useController, useForm } from "react-hook-form"function Input({ control, name }) {const {field,fieldState: { invalid, isTouched, isDirty },formState: { touchedFields, dirtyFields },} = useController({name,control,rules: { required: true },})return (<TextFieldonChange={field.onChange} // send the value to hook formonBlur={field.onBlur} // notify when the input is touched or blurredvalue={field.value} // input valuename={field.name} // send the input nameinputRef={field.ref} // send input ref, so we can focus on the input when an error appears/>)}
Integrating with global state
This library doesn't require you to rely on a state management library, but you can easily integrate with them.
import { useForm } from "react-hook-form"import { connect } from "react-redux"import updateAction from "./actions"function App(props) {const { register, handleSubmit, setValue } = useForm({defaultValues: {firstName: "",lastName: "",},})// Submit your data into Redux storeconst onSubmit = (data) => props.updateAction(data)return (<form onSubmit={handleSubmit(onSubmit)}><input {...register("firstName")} /><input {...register("lastName")} /><input type="submit" /></form>)}// Connect your component with reduxexport default connect(({ firstName, lastName }) => ({ firstName, lastName }), {updateAction,})(App)
Handle errors
React Hook Form provides an errors object to show you the errors in the form. errors' type will return given validation constraints. The following example showcases a required validation rule.
import { useForm } from "react-hook-form"export default function App() {const {register,formState: { errors },handleSubmit,} = useForm()const onSubmit = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><input{...register("firstName", { required: true })}aria-invalid={errors.firstName ? "true" : "false"}/>{errors.firstName?.type === "required" && (<p role="alert">First name is required</p>)}<input{...register("mail", { required: "Email Address is required" })}aria-invalid={errors.mail ? "true" : "false"}/>{errors.mail && <p role="alert">{errors.mail.message}</p>}<input type="submit" /></form>)}
Integrating with services
To integrate React Hook Form with a service, you can use the library's built-in submission handling. The <Form /> component allows you to easily send form data to an API endpoint or other service. Find out more about the Form component.
import { Form, useForm } from "react-hook-form"function App() {const { register, control } = useForm()return (<Formaction="/api/save" // Send post request with the FormData// encType={'application/json'} you can also switch to json objectonSuccess={() => {alert("Your application is updated.")}}onError={() => {alert("Submission has failed.")}}control={control}><input {...register("firstName", { required: true })} /><input {...register("lastName", { required: true })} /><button>Submit</button></Form>)}
Schema Validation
React Hook Form supports schema validation through the resolver option. A resolver runs your schema against the complete form data and returns typed values or validation errors. Supported libraries include:
For complete TypeScript examples with Zod and Yup, see the React Hook Form integration guides.
- Yup
- Zod
- Superstruct
- Joi
- Vest
- Class Validator
- io-ts
- Nope
- computed-types
- typanion
- Ajv
- TypeBox
- ArkType
- Valibot
- TypeSchema
- effect-ts
- VineJS
fluentvalidation-ts
- ata-validator
any library that implements Standard Schema
See the full, up-to-date list in the @hookform/resolvers repo.
Step 1: Install Yup into your project.
npm install @hookform/resolvers yup
Step 2: Prepare your schema for validation and register inputs with React Hook Form.
import { useForm } from "react-hook-form"import { yupResolver } from "@hookform/resolvers/yup"import * as yup from "yup"const schema = yup.object({firstName: yup.string().required(),age: yup.number().positive().integer().required(),}).required()export default function App() {const {register,handleSubmit,formState: { errors },} = useForm({resolver: yupResolver(schema),})const onSubmit = (data) => console.log(data)return (<form onSubmit={handleSubmit(onSubmit)}><input {...register("firstName")} /><p>{errors.firstName?.message}</p><input {...register("age")} /><p>{errors.age?.message}</p><input type="submit" /></form>)}
React Native
You will get the same performance boost and enhancement in React Native. To integrate with input component, you can wrap it with Controller.
import { Text, View, TextInput, Button } from "react-native"import { useForm, Controller } from "react-hook-form"export default function App() {const {control,handleSubmit,formState: { errors },} = useForm({defaultValues: {firstName: "",lastName: "",},})const onSubmit = (data) => console.log(data)return (<View><Controllercontrol={control}rules={{required: true,}}render={({ field: { onChange, onBlur, value } }) => (<TextInputplaceholder="First name"onBlur={onBlur}onChangeText={onChange}value={value}/>)}name="firstName"/>{errors.firstName && <Text>This is required.</Text>}<Controllercontrol={control}rules={{maxLength: 100,}}render={({ field: { onChange, onBlur, value } }) => (<TextInputplaceholder="Last name"onBlur={onBlur}onChangeText={onChange}value={value}/>)}name="lastName"/><Button title="Submit" onPress={handleSubmit(onSubmit)} /></View>)}
TypeScript
React Hook Form is built with TypeScript, and you can define a FormData type to support form values.
import { useForm } from "react-hook-form"type FormData = {firstName: stringlastName: string}export default function App() {const {register,setValue,handleSubmit,formState: { errors },} = useForm<FormData>()const onSubmit = handleSubmit((data) => console.log(data))// firstName and lastName will have correct typereturn (<form onSubmit={onSubmit}><label>First Name</label><input {...register("firstName")} /><label>Last Name</label><input {...register("lastName")} /><buttontype="button"onClick={() => {setValue("lastName", "luo") // ✅ accepts a string// setValue("firstName", true) // TypeScript error: expected string// errors.bill // TypeScript error: field does not exist}}>SetValue</button></form>)}
Design and philosophy
React Hook Form's design and philosophy focus on user and developer experience. The library aims to provide users with a smoother interaction experience by fine-tuning the performance and improving accessibility. Some of the performance enhancements include:
- Introducing form state subscription model through the proxy
- Avoiding unnecessary computation
- Isolating component re-rendering when required
Overall, it improves the user experience as users interact with the application. For developers, we introduced built-in validation and are closely aligned with HTML standards, allowing further extension with powerful validation methods and native integration with schema validation. In addition, having a strongly type-checked form with the help of TypeScript provides early build-time feedback to help and guide developers in building a robust form solution.
The following talk given by Bill showcased some of the ideas and design patterns:
Next steps
Choose the path that matches what you are building next.
Understand every useForm option, method, and form-state property.
Add schema validationConnect Zod, Yup, Valibot, or another supported resolver.
Build advanced formsCreate accessible, reusable, tested, and multi-step form experiences.
Solve common problemsFind answers about performance, controlled inputs, validation, and testing.