A 200-field React Hook Form form lags on every keystroke after watch() was added in the form component for a live summary — why, and what would you use instead?
answer
- who subscribed, and where
- root-level subscription
- isolate in a child
- read without subscribing
basics
~20 swatch() in React Hook Form's useForm component subscribes that component to value changes, so every keystroke re-renders all 200 fields. Move the read into a child with useWatch, or call getValues when you only need values at an event.
solid answer
~40 s`watch()` re-renders **at the level of `useForm`** — the documentation says it triggers a re-render at the root of the form. Without arguments it watches every field, so each keystroke re-renders the component that renders all 200 inputs. Fixes, in order of preference: move the summary into a child that calls `useWatch({ control, name: [...] })`, which isolates re-renders to that child; use `getValues()` in event handlers, because it reads current values without subscribing; use `useFormState({ control })` for flags such as `isDirty` so the root does not read them; and for side effects like autosave, use `subscribe` (since 7.55.0), which reacts to changes without re-rendering. Also check `mode: 'onChange'` with a resolver, which validates the schema on every keystroke.
code
tsx · 14 linesimport { useWatch, type Control } from 'react-hook-form';
type Order = { plan: 'basic' | 'pro'; seats: number };
const PRICE = { basic: 5, pro: 12 } as const;
// Only this component re-renders when plan or seats change.
export function PriceSummary({ control }: { control: Control<Order> }) {
const [plan, seats] = useWatch({ control, name: ['plan', 'seats'] });
return <p>Total: {PRICE[plan] * (seats || 0)} per month</p>;
}
// Inside the form component, instead of const all = watch():
// <PriceSummary control={control} />
// <button type="button" onClick={() => confirmOrder(getValues())}>Review</button>go deeper
Know the three names and which one subscribes: watch and useWatch do, getValues does not.
Explain that the difference between watch and useWatch is only the level that re-renders, and why that matters for field count.
Profile a lagging form, locate the root subscription, and move reads into useWatch, useFormState, getValues or subscribe as each case needs.
Set conventions for large forms, such as no root watch and isolated summary components, and back them with a profiling budget.
## Three ways to read values React Hook Form keeps field values outside React state, so reading them is a choice about **who gets re-rendered**. The three common tools differ exactly there: | API | Subscribes? | What re-renders | Typical use | |---|---|---|---| | `watch(names?)` | Yes | The component that called `useForm` | Small forms, conditional fields at the root | | `useWatch({ control, name })` | Yes | Only the component calling it | Live summaries, dependent widgets in large forms | | `getValues(names?)` | No | Nothing | Reading values inside handlers and callbacks | Two related tools complete the picture: - `useFormState({ control })` isolates **form-state** subscriptions (`isDirty`, `errors`, `isValid`) in a child, the way `useWatch` isolates values. - `subscribe({ name, formState, callback })`, added in 7.55.0, lets you react to value or form-state changes **without** re-rendering — right for autosave or analytics. ## Why the root watch hurts `watch()` is documented to trigger a re-render at the root of the form. Called without arguments it watches every field. In a 200-field form that means every keystroke re-renders the form component, which re-renders every field element beneath it. Registered inputs are cheap to re-render, but 200 of them per keystroke — plus any heavy widgets — shows up as input lag. The summary panel that needed the values is a small fraction of that work. ## Fixing it, step by step 1. **Confirm the subscriber.** Record a React profiler trace while typing; the form component will show as re-rendering on each keystroke. 2. **Move the summary into its own component** and read values there with `useWatch({ control, name: ['plan', 'seats'] })`. Only that component re-renders when those fields change. 3. **Narrow the names.** Watching specific fields instead of the whole form avoids re-renders for unrelated changes. 4. **Use `getValues()` where you only need a value at a moment** — inside a click handler, before a network call, in a confirm dialog. 5. **Move form-state reads out of the root** with `useFormState` in the component that needs them, such as the submit button. 6. **For side effects, use `subscribe`**, returning its unsubscribe function from an effect. ## Other things that make the root re-render - Reading `formState` properties at the root: the Proxy subscribes the root to each one it reads. - `mode: 'onChange'` or `'all'` with a resolver: validation runs on every keystroke, and when errors change, `errors` subscribers re-render; the resolver also runs against the whole form's values each time. - Controlled widgets are fine inside `Controller`, but lifting their value into root state with `useState` brings back the root re-render. ## Pitfalls with the replacements - **`useWatch` needs `control`** (or a `FormProvider` above it). - **Initial values.** `useWatch` returns the current form value when one exists; its `defaultValue` (or `useForm`'s `defaultValues`) is only a fallback before the form has mounted. Provide `defaultValues` so the first render is not `undefined`. - **`watch` results are for rendering**, not for `useEffect` dependency arrays; the documentation points you to comparison hooks or `subscribe` for detecting updates. - **`getValues` is not reactive.** Rendering `getValues('plan')` shows a value that goes stale; that is the trade you accept for zero subscriptions.
- In React Hook Form, when is watch() at the root still the right choice?In a small form where a value decides what renders at the root, such as showing a company field when an account type is 'business'. The root re-render is cheap there, and moving it into a child adds indirection for no gain. The cost matters as the field count or widget weight grows.
- Why does rendering getValues('plan') in JSX show a stale value in React Hook Form?`getValues` reads the current value but does not subscribe, so nothing re-renders the component when the value changes. It shows whatever was current during the last render that happened for some other reason. Use `useWatch` for anything displayed, and `getValues` inside handlers.
- What does useWatch return on its first render in React Hook Form?The current form value if one is available. Its own `defaultValue` option, or `useForm`'s `defaultValues`, is used only as a fallback before the form has mounted and no value exists yet. Supplying `defaultValues` to `useForm` keeps the first render from being `undefined`.
watch() at the root is a building-wide announcement for every change, heard in all 200 offices; useWatch is a message to the one office that asked, and getValues is walking over to read the noticeboard when you need it.
saying these in an interview costs you the question
- watch() only re-renders the input being watched
- getValues() re-renders the component when the value changes
- useWatch and watch have identical re-render cost
- Wrapping each input in React.memo is the main fix for a root watch
- The lag must come from register, so switch every field to Controller