Skip to content

React Hook Form Integrations

Use every official schema resolver plus practical integrations for Next.js, shadcn/ui, and React 19.

React Hook Form works with schema validation libraries, controlled UI components, and modern React frameworks without changing how your form data is managed. These examples show the smallest useful setup for popular integrations.

Zod

Use zodResolver to validate with a Zod schema. The resolver converts Zod issues into React Hook Form errors, so messages are available through formState.errors.

npm install react-hook-form @hookform/resolvers zod
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm, type SubmitHandler } from "react-hook-form"
import { z } from "zod"
const schema = z.object({
email: z.string().email("Enter a valid email address"),
age: z.number().min(18, "You must be at least 18"),
})
type FormValues = z.infer<typeof schema>
export function ProfileForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({ resolver: zodResolver(schema) })
const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
{errors.email && <p role="alert">{errors.email.message}</p>}
<input type="number" {...register("age", { valueAsNumber: true })} />
{errors.age && <p role="alert">{errors.age.message}</p>}
<button type="submit">Save</button>
</form>
)
}

valueAsNumber converts the input string before validation. If a schema transforms its input into a different output type, pass separate input and output generics to useForm: useForm<z.input<typeof schema>, unknown, z.output<typeof schema>>().

Yup

Use yupResolver when your project already models validation with Yup.

npm install react-hook-form @hookform/resolvers yup
import { yupResolver } from "@hookform/resolvers/yup"
import { useForm } from "react-hook-form"
import * as yup from "yup"
const schema = yup
.object({
name: yup.string().required("Name is required"),
email: yup.string().email().required("Email is required"),
})
.required()
type FormValues = yup.InferType<typeof schema>
export function ContactForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({ resolver: yupResolver(schema) })
return (
<form onSubmit={handleSubmit(console.log)}>
<input {...register("name")} />
{errors.name && <p role="alert">{errors.name.message}</p>}
<input {...register("email")} />
{errors.email && <p role="alert">{errors.email.message}</p>}
<button type="submit">Send</button>
</form>
)
}

Use one validation source for each rule. Keeping required, length, and format rules in the schema avoids maintaining the same rule in both register and Yup.

All schema resolvers

The @hookform/resolvers package supports the schema libraries below. Install React Hook Form, the resolver package, and the validation library you choose:

npm install react-hook-form @hookform/resolvers <validation-library>

Every adapter follows the same pattern:

import { useForm } from "react-hook-form"
import { libraryResolver } from "@hookform/resolvers/library"
const form = useForm({
resolver: libraryResolver(schema),
})

The inference column indicates whether the resolver can infer submitted values from the schema. criteriaMode indicates whether it can return only the first error per field or all field errors.

Schema libraryResolver import and callLibrary packageInfers valuescriteriaMode
AJVajvResolver(schema) from @hookform/resolvers/ajvajvNofirstError | all
ata-validatorataResolver(schema) from @hookform/resolvers/ata-validatorata-validatorNofirstError | all
ArkTypearktypeResolver(schema) from @hookform/resolvers/arktypearktypeYesfirstError
Class ValidatorclassValidatorResolver(schema) from @hookform/resolvers/class-validatorclass-validatorYesfirstError | all
computed-typescomputedTypesResolver(schema) from @hookform/resolvers/computed-typescomputed-typesYesfirstError
EffecteffectTsResolver(schema) from @hookform/resolvers/effect-tseffectYesfirstError | all
fluentvalidation-tsfluentValidationResolver(schema) from @hookform/resolvers/fluentvalidation-tsfluentvalidation-tsNofirstError
io-tsioTsResolver(schema) from @hookform/resolvers/io-tsio-tsYesfirstError
JoijoiResolver(schema) from @hookform/resolvers/joijoiNofirstError | all
NopenopeResolver(schema) from @hookform/resolvers/nopenope-validatorNofirstError
Standard SchemastandardSchemaResolver(schema) from @hookform/resolvers/standard-schemaA Standard Schema-compatible libraryYesfirstError | all
SuperstructsuperstructResolver(schema) from @hookform/resolvers/superstructsuperstructYesfirstError
typaniontypanionResolver(schema) from @hookform/resolvers/typaniontypanionYesfirstError
TypeBoxtypeboxResolver(schema) from @hookform/resolvers/typebox@sinclair/typeboxYesfirstError | all
TypeSchematypeschemaResolver(schema) from @hookform/resolvers/typeschema@typeschema/main plus a supported schema libraryNofirstError | all
ValibotvalibotResolver(schema) from @hookform/resolvers/valibotvalibotYesfirstError | all
VestvestResolver(schema) from @hookform/resolvers/vestvestNofirstError | all
VineJSvineResolver(schema) from @hookform/resolvers/vine@vinejs/vineYesfirstError | all
YupyupResolver(schema) from @hookform/resolvers/yupyupYesfirstError | all
ZodzodResolver(schema) from @hookform/resolvers/zodzodYesfirstError | all

Resolver-specific notes

  • AJV and ata-validator use JSON Schema objects. With AJV, use constraints such as minLength: 1 as well as required when an empty string should fail validation.
  • TypeBox accepts a TypeBox schema or a compiled TypeCheck. Register custom TypeBox kinds and formats before passing a schema that uses them.
  • Standard Schema works with libraries that implement the Standard Schema interface, including compatible versions of Zod and ArkType.
  • TypeSchema provides a common adapter over its supported validation libraries; install the underlying schema library too.
  • Effect uses effectTsResolver and currently targets the version documented by the resolver package. Check the official resolver example when upgrading Effect across major versions.
  • Class Validator uses a decorated class as its schema and requires the decorator settings documented by Class Validator.
  • Yup context belongs on useForm({ context }); do not put it in yupResolver schema options because form context takes precedence.

Resolvers accept an optional third argument for resolver behavior. Async validation is the default; use { mode: "sync" } only when the schema performs no asynchronous work. { raw: true } returns raw input values instead of the schema's parsed output.

Next.js

In the Next.js App Router, call useForm from a Client Component. You can submit with handleSubmit, or pass a Server Action to React Hook Form's Form component.

// app/profile/actions.ts
"use server"
export async function updateProfile(formData: FormData) {
const name = String(formData.get("name") ?? "")
if (!name) throw new Error("Name is required")
// Authenticate, validate again on the server, and update the database.
}
// app/profile/profile-form.tsx
"use client"
import { Form, useForm } from "react-hook-form"
import { updateProfile } from "./actions"
export function ProfileForm() {
const {
control,
register,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<{ name: string }>({ progressive: true })
return (
<Form control={control} action={updateProfile}>
<input {...register("name", { required: "Name is required" })} />
{errors.name && <p role="alert">{errors.name.message}</p>}
{errors.root?.server && <p role="alert">Could not save your profile.</p>}
{isSubmitSuccessful && <p>Profile saved.</p>}
<button disabled={isSubmitting}>Save</button>
</Form>
)
}

Client-side validation improves feedback, but it is not a security boundary. Authenticate the request and validate untrusted values again inside the Server Action. See Server Actions and useActionState for a lower-level pattern.

shadcn/ui

The shadcn/ui Form component composes React Hook Form's FormProvider and Controller. Define defaultValues for controlled fields, then pass the object returned by useForm to the UI component.

"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const schema = z.object({
username: z.string().min(2, "Enter at least two characters"),
})
export function UsernameForm() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { username: "" },
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(console.log)}>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Save</Button>
</form>
</Form>
)
}

The shadcn/ui Form above provides context; it is different from React Hook Form's submission-oriented Form component. Use register directly for native inputs and FormField or Controller for controlled components.

React 19

React 19 form actions and React Hook Form can be used together. Use progressive: true to render native validation attributes before hydration, and pass a function to the Form component's action prop.

"use client"
import { Form, useForm } from "react-hook-form"
import { createAccount } from "./actions"
export function AccountForm() {
const {
control,
register,
formState: { errors, isSubmitting },
} = useForm<{ email: string }>({ progressive: true })
return (
<Form control={control} action={createAccount}>
<input
type="email"
{...register("email", { required: "Email is required" })}
/>
{errors.email && <p role="alert">{errors.email.message}</p>}
<button disabled={isSubmitting}>Create account</button>
</Form>
)
}

Use React's useActionState when the action's returned result drives additional UI. Use formState when you mainly need validation errors, dirty fields, submitting state, and submission success from React Hook Form.

Choose an integration

GoalRecommended API
Validate with Zod or YupA resolver passed to useForm
Use a controlled UI componentController or useController
Share form methods through a component treeFormProvider and useFormContext
Submit to an endpoint or Server ActionForm
Own the complete request lifecyclehandleSubmit
Build repeatable dynamic rowsuseFieldArray

For the fundamentals, start with the React Hook Form getting-started guide. For reusable patterns, accessibility, testing, and Server Actions, continue to advanced usage.