FormProvider performance
Keep components under FormProvider from re-rendering on every change.
FormProvider shares the methods returned by useForm through React Context, so nested components can call useFormContext instead of receiving props. The context itself is cheap. What decides how often your form re-renders is where you read form state.
How re-renders spread
The component that calls useForm (the host) re-renders when a piece of formState it reads changes. For example, destructuring formState: { errors } re-renders the host whenever an error is added or removed. When the host re-renders:
- every child it renders inline re-renders too, as in any React component, and
- the context value changes, so every component that calls
useFormContextre-renders, even insidememo.
If the host does not read formState, typing and validation do not re-render it. Only the components that subscribe to the value or state they display update.
Subscribe in the leaves
Keep the host lean and move every read of form state into the component that renders it:
| To render… | Use |
|---|---|
| A field's current value | useWatch with name |
Errors, isSubmitting, isDirty | useFormState, with name to narrow it |
| A value inside an event handler | getValues, which never subscribes |
| A side effect when a value changes | subscribe in an effect |
import { memo } from "react"import {FormProvider,useForm,useFormContext,useFormState,useWatch,} from "react-hook-form"type FormValues = { firstName: string; bio: string }export default function ProfileForm({onSave,}: {onSave: (values: FormValues) => Promise<void>}) {// Don't read formState here. This component and everything that uses the// context would re-render on every change you subscribe to.const methods = useForm<FormValues>({defaultValues: { firstName: "", bio: "" },mode: "onChange",})return (<FormProvider {...methods}><form noValidate onSubmit={methods.handleSubmit(onSave)}><ProfileFields /><BioCounter /><SubmitButton /></form></FormProvider>)}// Registers inputs only, so it never needs to re-render after mounting.const ProfileFields = memo(function ProfileFields() {const { register } = useFormContext<FormValues>()return (<><label htmlFor="firstName">First name</label><inputid="firstName"{...register("firstName", { required: "Enter your first name." })}/><label htmlFor="bio">Bio</label><textareaid="bio"{...register("bio", {maxLength: { value: 200, message: "Use 200 characters or fewer." },})}/></>)})// Re-renders on every keystroke in the bio, and nothing else does.function BioCounter() {const bio = useWatch<FormValues, "bio">({ name: "bio" })return <p>{bio.length} / 200</p>}// Re-renders only when isSubmitting changes.function SubmitButton() {const { isSubmitting } = useFormState<FormValues>()return (<button type="submit" disabled={isSubmitting}>Save</button>)}
In this form, typing in the bio re-renders only BioCounter, and submitting re-renders only SubmitButton. ProfileForm and ProfileFields render once. If ProfileForm read formState.errors, the first validation error would re-render all four components.
memo on ProfileFields stops re-renders that come from a parent re-rendering for other reasons, such as unrelated state. It cannot stop re-renders caused by the context changing, so a lean host matters more than memo.
Show errors without re-rendering the form
Render each error message in its own component that calls useFormState({ name }). Only that message re-renders when its field's error changes. See the FieldMessage component in Error messages.
Pass props instead of context
For a hot path, you can skip context and pass register or control down as props. These functions keep the same reference for the life of the form, so a memo child that receives only them never re-renders because of the form.
DevTools
DevTools subscribes to the whole form state and re-renders on every change, which can make a large form feel slow. Remove it before measuring performance.
Measure first
Use the React DevTools Profiler, or add a temporary console.count to a component, before optimizing. Most forms are fast enough without any of these changes. Reach for them when a large form or an expensive component makes typing feel slow.