Connect form
Give deeply nested inputs access to form methods.
When inputs live deep in a component tree, passing register, control, and formState through every level gets noisy. Wrap the form in FormProvider and read the methods anywhere below it with useFormContext.
A typed hook per form
useFormContext needs your form's type as a generic. Instead of repeating useFormContext<CheckoutValues>() in every component, wrap it in a hook named after the form. The hook is also a good place to fail loudly: outside a FormProvider, useFormContext returns null, and the first register call would otherwise throw a confusing error.
A render-prop connector
A ConnectForm component passes the methods to a function child. It is handy for one-off inputs inside JSX, or in code that cannot call hooks directly, such as a class component's render method.
import type { ReactNode } from "react"import {FormProvider,useForm,useFormContext,type FieldValues,type UseFormReturn,} from "react-hook-form"type CheckoutValues = {email: stringshipping: { street: string; city: string }}// A typed hook for this form, so components don't repeat the generic.export function useCheckoutForm() {const methods = useFormContext<CheckoutValues>()if (!methods) {throw new Error("useCheckoutForm must be used inside <FormProvider>.")}return methods}// A render-prop version for places where you can't call a hook.export function ConnectForm<T extends FieldValues>({children,}: {children: (methods: UseFormReturn<T>) => ReactNode}) {return children(useFormContext<T>())}function ShippingAddress() {const {register,formState: { errors },} = useCheckoutForm()return (<fieldset><legend>Shipping address</legend><label htmlFor="street">Street</label><inputid="street"{...register("shipping.street", { required: "Enter a street." })}/>{errors.shipping?.street && (<p role="alert">{errors.shipping.street.message}</p>)}<label htmlFor="city">City</label><inputid="city"{...register("shipping.city", { required: "Enter a city." })}/></fieldset>)}export default function Checkout({onSubmit,}: {onSubmit: (values: CheckoutValues) => void}) {const methods = useForm<CheckoutValues>({defaultValues: { email: "", shipping: { street: "", city: "" } },})return (<FormProvider {...methods}><form noValidate onSubmit={methods.handleSubmit(onSubmit)}><ConnectForm<CheckoutValues>>{({ register }) => (<><label htmlFor="email">Email</label><input id="email" type="email" {...register("email")} /></>)}</ConnectForm><ShippingAddress /><button type="submit">Place order</button></form></FormProvider>)}
ShippingAddress uses the typed hook, so register("shipping.street") is checked against CheckoutValues, and a typo is a type error. The email input uses the render-prop connector inline.
When to use which
| Situation | Use |
|---|---|
| A component inside one known form | A typed hook such as useCheckoutForm |
| Inline JSX, or a component that cannot call hooks | ConnectForm |
| A reusable input used in many different forms | Pass control and name as props and call useController |
| A component that only displays a value or form state | useWatch or useFormState, which accept control or read it from context |
Reading formState from context subscribes the component the same way reading it from useForm does. To keep re-renders low, read only what the component renders. See FormProvider performance.
Nested providers
Each useFormContext reads the nearest FormProvider above it. If you nest two forms, for example a dialog form inside a page form, components inside the dialog read the inner one. Render the inner form outside the outer <form> element, such as in a portal, because HTML does not allow nested <form> elements.