Why one return value re-renders the whole component and the other does not
React Hook Form advertises avoiding unnecessary re-renders by managing input fields uncontrolled through refs instead of state. That exact advantage disappears quickly, though, the moment watch() enters the picture, since the value this function returns triggers a re-render of the entire form component on every keystroke, while the dedicated useWatch() hook solves exactly this problem in a targeted way.
Table of Contents
- 1. Why React Hook Form uses uncontrolled fields at all
- 2. How watch() works and why it affects the whole component
- 3. How useWatch() solves the same problem in a targeted way
- 4. Making the difference measurable
- 5. When watch() is still perfectly fine
- 6. Further useWatch() variants for concrete use cases
- 7. useWatch() and Controller working together
- 8. Migrating existing code from watch() to useWatch()
- 9. Deciding based on form size
- 10. Summary
- 11. FAQ
1. Why React Hook Form uses uncontrolled fields at all
React Hook Form differs from most other form libraries in that input fields stay uncontrolled by default: instead of writing every keystroke into React state via useState and re-rendering on every keystroke as a result, the library registers fields through refs directly on the DOM element and only reads their values on demand, for example when the form is submitted. That is the main reason React Hook Form is noticeably more performant than state-based alternatives for large forms with many fields.
That advantage only holds, however, as long as you do not actively work against it. As soon as a component wants to observe a field's current value while the user is typing, for example to drive a conditional display or show a live preview, React Hook Form necessarily has to route that value back into React state, since only that can trigger a re-render at all. That is exactly the point where it gets decided whether only the relevant spot re-renders or the entire form component, and this is exactly where watch() and useWatch() fundamentally differ.
2. How watch() works and why it affects the whole component
watch() is a function returned by the useForm() hook that, when called, returns the current value of one or more fields. The crucial point is where this call typically sits: directly in the function body of the component that also called useForm(). React Hook Form internally subscribes to changes on that field and triggers an internal state update in exactly that component on every change, so that watch() can return the current value on its next call.
The consequence: every component that calls watch() fully re-renders on every keystroke into the observed field, including every other form field rendered inside that same component. For a small form with three fields that is barely noticeable, but for a large form with fifty fields, several conditional sections, and expensively rendered options, every single keystroke into one observed field can re-render the entire form component, including every unrelated field along with it.
function LargeForm() {
const { register, watch } = useForm();
// Every keystroke into "country" re-renders the ENTIRE component,
// including every field registered below
const country = watch("country");
return (
<form>
<input {...register("country")} />
{country === "DE" && <input {...register("zip")} />}
<input {...register("company")} />
<input {...register("street")} />
{/* ... 40 more fields, all re-rendering along with it ... */}
</form>
);
}
3. How useWatch() solves the same problem in a targeted way
useWatch() is a standalone hook that serves the same purpose as watch(), returning the current field value, but works differently under the hood: it subscribes to the field change through its own internal subscription mechanism that is independent of the parent component. Called inside a dedicated, small subcomponent, a change to the observed field then only triggers a re-render of that small subcomponent, not of the parent form component and all its other fields.
So the difference is not in the returned value, both return the current field value, it lies exclusively in which component re-renders as a result. By moving useWatch() into a dedicated component, the re-render radius can be limited precisely to the part of the interface that actually displays the observed value or needs it for conditional logic, while the rest of the form stays completely untouched.
function LargeForm() {
const { register, control } = useForm();
return (
<form>
<input {...register("country")} />
{/* Only CountryDependentField re-renders on change */}
<CountryDependentField control={control} />
<input {...register("company")} />
<input {...register("street")} />
{/* ... 40 more fields, untouched by this change ... */}
</form>
);
}
function CountryDependentField({ control }) {
const country = useWatch({ control, name: "country" });
return country === "DE" ? <input name="zip" /> : null;
}
4. Making the difference measurable
The effect can be made directly visible with the already familiar React DevTools option Highlight updates when components render: with watch(), the entire form component including every child field flashes on every keystroke, with useWatch() inside a dedicated subcomponent only that small component stays visibly lit while the rest of the form remains stable. For forms with noticeable typing lag, that is often the fastest way to narrow down the cause.
For a solid statement about the actual milliseconds, a short profiler recording during a typical typing sequence is worth adding on top. For forms with computationally expensive rendered fields, for example a rich-text editor, a map component, or a long select list with server-filtered options, the difference between the two approaches can be the difference between a smooth form and a noticeably janky one.
5. When watch() is still perfectly fine
watch() is by no means to be avoided on principle. For small forms with few fields, where the whole form component stays manageable anyway, the difference between watch() and useWatch() is barely measurable, and the more direct, shorter watch() call inside the component body saves unnecessary indirection through a dedicated subcomponent. For one-off reads outside the render cycle too, for example watch() with no arguments inside an event handler to read all current values, no additional re-render problem arises at all.
watch() only becomes problematic once its return value is used directly in the render path of a large, expensively rendered form component while only a small part of the interface actually depends on that value. The rule of thumb is therefore: the bigger the form and the more expensive its other fields are to render, the more it pays off to move the observation into useWatch() with a dedicated subcomponent.
6. Further useWatch() variants for concrete use cases
Besides observing a single field name, useWatch() also supports an array of field names to observe several values at once, as well as a call with no name parameter at all to observe the entire form object, which is useful for a live preview of the whole form state. In addition, defaultValue lets you specify an initial value used before the actual form is fully initialized, which avoids flickering on the first render.
For the frequent case where a computed value needs to be derived from several fields, for example a total sum from multiple quantity fields, combining useWatch() with an array of field names and a dedicated subcomponent that displays only that computation is the recommended approach. That keeps the expensive recalculation and its re-render limited exactly to the total display, while the individual input fields themselves stay uncontrolled and therefore render-free.
function TotalPreview({ control }) {
const [price, quantity] = useWatch({
control,
name: ["price", "quantity"],
});
const total = (Number(price) || 0) * (Number(quantity) || 0);
return <p>Total: {total.toFixed(2)} EUR</p>;
}
7. useWatch() and Controller working together
For controlled third-party components, for example a datepicker or select library that itself expects a value and onChange prop, React Hook Form typically uses the Controller component. The mechanism Controller uses internally resembles useWatch(): here too, the re-render stays limited to the respective registered controlled component instead of affecting the entire form component.
If the current value of a field managed by Controller is additionally needed elsewhere in the form, for example for a conditional display, useWatch() with the same field name is the right addition, rather than querying the value again through watch() at the component level. That way Controller and useWatch() stay consistent within the same isolation principle, without any part of the form accidentally reintroducing the old, expensive watch() coupling.
8. Migrating existing code from watch() to useWatch()
A migration can be carried out step by step, without rebuilding the entire form at once: first, use DevTools highlighting to identify which watch() calls actually cause noticeable re-renders of the large form component. Then, move exactly the part of the interface that depends on the observed value into its own small component, one that receives control as a prop and uses useWatch() internally instead of having the value passed down from the parent component.
It matters to consistently pass control down to the new subcomponent through props or the FormProvider context, rather than calling useForm() there a second time, which would create an inconsistent, independent form instance. After the migration, a fresh look at component highlighting confirms that only the extracted subcomponent flashes on keystrokes while the rest of the form stays stable.
9. Deciding based on form size
In summary, the right choice depends less on a general rule and more on the specific form size and the render cost of the remaining fields. For prototypes, small forms, and one-off reads outside the render path, watch() remains the pragmatic, uncomplicated choice, without introducing unnecessary indirection through additional subcomponents.
For production forms with many fields, conditional sections, or computationally expensive child components, useWatch() inside a dedicated subcomponent is almost always the superior choice, since the performance difference grows disproportionately with form size. The table below sums up this decision once more, concretely.
| Criterion | watch() | useWatch() | Recommendation |
|---|---|---|---|
| Re-render radius | The entire component that calls watch() | Only the component that calls useWatch() | Use useWatch() in a subcomponent for large forms |
| Setup effort | Direct call inside the component body | Needs a dedicated subcomponent with a control prop | Prefer watch() for small forms |
| Observing multiple fields at once | Array of field names as an argument | Array of field names as the name option | Both equivalent, choice depends on where it renders |
| One-off read inside a handler | Well suited, no re-render issue | Not meant for this, it is a reactive hook | Clearly use watch() for event handlers |
Mironsoft
React architecture, performance, and Magento frontend integration
React frontends that stay fast instead of slowing down with every feature?
We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.
Performance Audit
Systematically measuring and fixing re-renders, bundle size, and load times.
State Architecture
Cleanly separating context, client state, and server state instead of mixing everything.
Magento Integration
Building robust, type-safe GraphQL or REST integration with Magento.
10. Summary
useWatch vs. watch(): The Essentials at a Glance
watch()
Triggers a re-render of the entire calling component on every keystroke into the observed field.
useWatch()
Isolates the re-render to the component where the hook itself is called, independent of the parent.
Rule of thumb
Small forms tolerate watch() fine, large forms with many fields need useWatch() in subcomponents.
Verification
React DevTools highlighting instantly shows whether only the relevant subcomponent or the whole form flashes.