React Hook Form 09 ๐ Watching Values โ watch vs useWatch for selective subscriptions
The watch function and the useWatch hook read the same form values, but they subscribe at different scopes. The watch function is called on the useForm return object, and it triggers a re-render at the component that owns the useForm call. The useWatch hook is called inside a child component, and it triggers a re-render only at that child component. The values are identical; the difference is the render boundary, and the boundary is the mechanism that determines whether a keystroke in one field re-renders the whole form or only the display that reads that field.
The React Hook Form documentation states the distinction plainly: useWatch “behaves similarly to the watch API; however, it isolates re-rendering at the custom hook level, which can result in better performance for your application” . The FAQ adds the decision rule: watch re-renders “the component that calls useForm“, and useWatch re-renders “only the component that calls it” . The rule is simple: if the value is read in the component that owns the form, watch is convenient; if the value is read in a nested component, useWatch is the performance choice.
The library also exposes two subscription mechanisms that do not re-render at all. The getValues function reads the current values on demand, and it is the mechanism for the event handlers and the effects. The watch callback form (or the subscribe method) runs a callback on every change, and it is the mechanism for the autosave, the analytics, and the other side effects that do not need the component to re-render.
Key point: watch subscribes at the useForm level and re-renders the component that owns the form. useWatch subscribes at the component level and re-renders only the component that calls the hook. The values are the same; the difference is the render scope. The getValues function reads the values without a subscription, and the watch callback form (or the subscribe method) is for the side effects that do not need a re-render.
Why the two APIs exist
The render-scope problem. A form with a live total that updates as the user types needs to read the values during the render. If the total is rendered in the same component that owns the useForm call, the watch("items") call re-renders the whole form on every item change. If the total is extracted into a child component, the useWatch({ control, name: "items" }) call re-renders only the total component. The extraction is the mechanism that isolates the re-render, and the control prop is the connection between the child and the form.
The subscription-timing problem. The useWatch hook subscribes when the component mounts. If the value is set before the subscription is in place, the update is ignored. The documentation warns about this: setValue("test", "data") followed by useWatch({ name: "test" }) does not receive the update, because the subscription happened after the value change . The custom hook that merges useWatch() with getValues() is the workaround, because the getValues() provides the current value even when the subscription missed the update.
The side-effect problem. A form that autosaves or tracks analytics needs to react to the value changes without re-rendering. The watch callback form (or the subscribe method) is the mechanism: the callback runs on every change, and no component re-renders . The type argument in the callback distinguishes the user changes ("change") from the programmatic changes (undefined for setValue and reset), which is the mechanism that lets the autosave ignore the programmatic updates.
The React Compiler problem. The watch and the getValues return values keep the same reference across renders, and the React Compiler may cache the result and render the stale values. The documentation recommends the useWatch for the values that are rendered in the compiled components, and the subscribe for the side effects . The "use no memo" directive is the escape hatch for the component that must keep the watch call.
The derived-value problem. A total that depends on several fields re-renders on every change to any of the fields, even when the total does not change. The compute option (since v7.61.0) is the mechanism that re-renders only when the computed result changes, not on every keystroke . The compute function receives the watched value and returns the derived value, and the deep comparison is the mechanism that decides whether the re-render is needed.
a. The watch function and the form-level subscription
The watch function is the method on the useForm return object. It has the four overloads: the single field, the array of fields, the entire form, and the callback form.
// The watch function overloads
import { useForm } from "react-hook-form";
type CheckoutFormValues = {
items: { price: number }[];
coupon: string;
};
function CheckoutForm() {
const { register, watch } = useForm<CheckoutFormValues>({
defaultValues: { items: [], coupon: "" },
});
// The single field: re-renders the form on every change to coupon.
const coupon = watch("coupon");
// The array of fields: re-renders on every change to either field.
const [items, couponValue] = watch(["items", "coupon"]);
// The entire form: re-renders on every change to any field.
const allValues = watch();
return (
<form>
<input {...register("coupon")} />
<p>Coupon: {coupon}</p>
</form>
);
}
The watch call in the component body is the render-level subscription. The component re-renders on every change to the watched fields, and the watch call returns the current value . The re-render scope is the component that owns the useForm call, which is the reason the watch in the render body of a large form is the performance problem.
The callback form is the subscription without the re-render. The callback receives the values and the { name, type } object, and the unsubscribe function is the cleanup.
// The callback form
import { useEffect } from "react";
import { useForm } from "react-hook-form";
function AutosaveForm() {
const { register, watch } = useForm();
useEffect(() => {
const subscription = watch((values, { name, type }) => {
// The type is "change" for the user input.
// The type is undefined for the setValue and the reset.
if (type === "change") {
// The autosave runs without a re-render.
console.log("Autosave:", values);
}
});
return () => subscription.unsubscribe();
}, [watch]);
return <form><input {...register("name")} /></form>;
}
The type argument is the mechanism that distinguishes the user input from the programmatic change. The "change" is the user input, and the undefined is the setValue or the reset . The distinction is the mechanism that lets the autosave ignore the programmatic updates that would otherwise cause the redundant API calls.
b. The useWatch hook and the isolated subscription
The useWatch hook is called inside the child component, and it receives the control object from the useForm call. The hook returns the current value, and the child component re-renders only when the watched value changes.
// The useWatch hook
import { useForm, useWatch, type Control } from "react-hook-form";
function OrderTotal({ control }: { control: Control<CheckoutFormValues> }) {
// The useWatch subscribes to the items field.
// Only this component re-renders when items changes.
const items = useWatch({ control, name: "items" });
const coupon = useWatch({ control, name: "coupon" });
const total = items.reduce((sum, item) => sum + item.price, 0);
const discount = coupon === "SAVE10" ? total * 0.1 : 0;
return <p>Total: ${total - discount}</p>;
}
function CheckoutForm() {
const { register, control } = useForm<CheckoutFormValues>({
defaultValues: { items: [], coupon: "" },
});
return (
<form>
<input {...register("coupon")} />
{/* The OrderTotal re-renders, the CheckoutForm does not. */}
<OrderTotal control={control} />
</form>
);
}
The control prop is the connection between the child and the form. The useWatch hook reads the values through the control object, and the child component re-renders only when the watched values change . The CheckoutForm component does not re-render on the keystrokes, which is the performance benefit.
The defaultValue option is the fallback before the form is mounted. The option is used only when the form has no current value, and the actual value takes precedence once the form is mounted .
// The defaultValue fallback
function NameDisplay({ control }: { control: Control<FormValues> }) {
// The defaultValue is "Guest" before the form is mounted.
// The actual value replaces it once the form is mounted.
const name = useWatch({ control, name: "name", defaultValue: "Guest" });
return <p>{name}</p>;
}
The exact option (since v7.20.0, with the behavior refined in v7.81.0) is the mechanism that controls the prefix matching. The default exact: false fires the subscription when the watched name is a prefix of the changed field, so the useWatch({ name: "users" }) receives the updates for the users.0.name . The exact: true is the mechanism that restricts the subscription to the exact field or the ancestor paths.
// The exact option
function UserCount({ control }: { control: Control<FormValues> }) {
// The exact: true means the subscription fires when the users array
// is set, but not when the users.0.name is set.
const users = useWatch({ control, name: "users", exact: true });
return <p>{users?.length ?? 0} users</p>;
}
The compute option (since v7.61.0) is the mechanism that re-renders only when the derived value changes. The function receives the watched value and returns the derived value, and the deep comparison is the mechanism that decides whether the re-render is needed .
// The compute option
function ItemCount({ control }: { control: Control<FormValues> }) {
// The compute returns the item count.
// The component re-renders only when the count changes,
// not on every keystroke in the item name.
const count = useWatch({
control,
name: "items",
compute: (items = []) => items.length,
});
return <p>{count} items</p>;
}
The compute is the mechanism that reduces the re-render frequency from the every-keystroke to the value-change. The total that depends on several fields is the common case, and the compute is the mechanism that makes the total component re-render only when the total changes.
Complete Example Session
// ============================================
// PART 1: THE WATCH RE-RENDER SCOPE
// ============================================
import { useForm } from "react-hook-form";
function CheckoutForm() {
const { register, watch } = useForm<CheckoutFormValues>({
defaultValues: { items: [], coupon: "" },
});
// The watch in the form component re-renders the whole form.
const coupon = watch("coupon");
return (
<form>
<input {...register("coupon")} />
<p>Coupon: {coupon}</p>
</form>
);
}
// ============================================
// PART 2: THE USEWATCH ISOLATED SCOPE
// ============================================
import { useWatch, type Control } from "react-hook-form";
function CouponDisplay({ control }: { control: Control<CheckoutFormValues> }) {
// The useWatch re-renders only this component.
const coupon = useWatch({ control, name: "coupon" });
return <p>Coupon: {coupon}</p>;
}
function CheckoutForm() {
const { register, control } = useForm<CheckoutFormValues>({
defaultValues: { items: [], coupon: "" },
});
return (
<form>
<input {...register("coupon")} />
{/* The CouponDisplay re-renders, the CheckoutForm does not. */}
<CouponDisplay control={control} />
</form>
);
}
// ============================================
// PART 3: THE GETVALUES NO SUBSCRIPTION
// ============================================
function SubmitButton({ getValues }: { getValues: () => CheckoutFormValues }) {
const handleClick = () => {
// The getValues reads the current values without a subscription.
const values = getValues();
console.log(values.coupon);
};
return <button type="button" onClick={handleClick}>Log</button>;
}
// ============================================
// PART 4: THE SUBSCRIBE SIDE EFFECT
// ============================================
import { useEffect } from "react";
function AutosaveForm() {
const { register, watch } = useForm();
useEffect(() => {
const subscription = watch((values, { name, type }) => {
// The callback runs without a re-render.
// The type is "change" for the user input.
if (type === "change") {
console.log("Autosave:", name, values[name!]);
}
});
return () => subscription.unsubscribe();
}, [watch]);
return <form><input {...register("name")} /></form>;
}
// ============================================
// PART 5: THE DEFAULT VALUE FALLBACK
// ============================================
function NameDisplay({ control }: { control: Control<FormValues> }) {
// The defaultValue is the fallback before the form is mounted.
const name = useWatch({ control, name: "name", defaultValue: "Guest" });
return <p>{name}</p>;
}
// ============================================
// PART 6: THE EXACT OPTION
// ============================================
function UserCount({ control }: { control: Control<FormValues> }) {
// The exact: true restricts the subscription to the exact field.
const users = useWatch({ control, name: "users", exact: true });
return <p>{users?.length ?? 0} users</p>;
}
// ============================================
// PART 7: THE COMPUTE OPTION
// ============================================
function ItemCount({ control }: { control: Control<FormValues> }) {
// The compute returns the derived value.
// The component re-renders only when the count changes.
const count = useWatch({
control,
name: "items",
compute: (items = []) => items.length,
});
return <p>{count} items</p>;
}
// ============================================
// PART 8: THE SUBSCRIPTION TIMING
// ============================================
// โ The subscription happens after the value update.
setValue("test", "data");
useWatch({ name: "test" }); // The update is ignored.
// โ
The subscription happens before the value update.
useWatch({ name: "example" });
setValue("example", "data"); // The update is received.
// ============================================
// PART 9: THE REACT COMPILER
// ============================================
// The watch result is not safe with the React Compiler.
// The compiler may cache the result and render the stale values.
// Use the useWatch for the values that are rendered,
// and the subscribe for the side effects.
// ============================================
// PART 10: THE CHECKLIST
// ============================================
// [ ] The watch is used in the form component that reads the values.
// [ ] The useWatch is used in the child component that reads the values.
// [ ] The control is passed from the useForm to the child.
// [ ] The defaultValue is set for the fallback.
// [ ] The compute is used for the derived values.
// [ ] The subscription timing is checked.
The ten parts covered the watch re-render scope, the useWatch isolated scope, the getValues no subscription, the subscribe side effect, the defaultValue fallback, the exact option, the compute option, the subscription timing, the React Compiler, and the checklist. The through-line is that the two APIs read the same values, and the render boundary is the difference.
Quick Reference
The API comparison
| API | Re-renders | Use for |
|---|---|---|
watch | The component that calls useForm | The form-level rendering |
useWatch | Only the component that calls the hook | The nested rendering |
getValues | Nothing | The event handlers |
watch callback / subscribe | Nothing | The side effects |
The useWatch options
| Option | Purpose |
|---|---|
name | The field or the fields to watch |
control | The form control object |
defaultValue | The fallback before the form is mounted |
exact | The exact name matching |
compute | The derived value subscription |
disabled | The subscription disable |
The watch overloads
| Overload | Returns |
|---|---|
watch(name) | The single field value |
watch(names) | The array of field values |
watch() | The entire form values |
watch(callback) | The unsubscribe function |
The subscription timing
| Order | Result |
|---|---|
setValue then useWatch | The update is missed |
useWatch then setValue | The update is received |
Best Practices
โ Do This:
// Use the useWatch in the child component
const name = useWatch({ control, name: "name" }); // โ
// Use the compute for the derived values
const count = useWatch({ control, name: "items", compute: (i) => i.length }); // โ
// Use the getValues in the event handler
const handleClick = () => getValues(); // โ
โ Don’t Do This:
// Use the watch in the render body of a large form
const allValues = watch(); // the whole form re-renders // โ
// Use the useWatch after the setValue
setValue("test", "data"); useWatch({ name: "test" }); // โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The whole form re-renders on every keystroke | The watch is used in the form component | Use the useWatch in the child |
The useWatch returns undefined on the first render | The field is not registered | Set the defaultValue or the defaultValues |
The useWatch does not receive the update | The subscription happens after the setValue | Subscribe before the update, or merge with the getValues |
| The re-render is too frequent | The watched value changes on every keystroke | Use the compute option |
Real-World Examples
1. The watch in the form
const coupon = watch("coupon");
2. The useWatch in the child
const coupon = useWatch({ control, name: "coupon" });
3. The getValues in the handler
const handleClick = () => console.log(getValues());
4. The subscribe side effect
watch((values, { type }) => { if (type === "change") autosave(values); });
5. The defaultValue
useWatch({ control, name: "name", defaultValue: "Guest" });
6. The exact option
useWatch({ control, name: "users", exact: true });
7. The compute option
useWatch({ control, name: "items", compute: (i) => i.length });
8. The control prop
<OrderTotal control={control} />
9. The subscription timing
useWatch({ name: "example" }); setValue("example", "data");
10. The custom hook merge
const useFormValues = () => ({ ...useWatch(), ...getValues() });
Visual
The Render Scope
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE RENDER SCOPE โ
โ โ
โ watch in the form component โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ The whole form re- โ Every keystroke re-renders โ
โ โ renders โ the form component โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ useWatch in the child component โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Only the child re- โ The form component does not โ
โ โ renders โ re-render โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Subscription Timing
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE SUBSCRIPTION TIMING โ
โ โ
โ โ The subscription after the update: โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ setValue("test") โ The value is set โ
โ โ useWatch("test") โ The subscription is too late โ
โ โ โ The update is ignored โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โ
The subscription before the update: โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ useWatch("example") โ The subscription is in place โ
โ โ setValue("example") โ The update is received โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Compute Option
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE COMPUTE OPTION โ
โ โ
โ Without the compute: โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ useWatch("items") โ The re-render on every keystroke โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ With the compute: โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ compute: (items) => โ The re-render only when the โ
โ โ items.length โ count changes โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
watch | The form-level subscription, re-renders the useForm component |
useWatch | The component-level subscription, re-renders the hook component |
getValues | The on-demand read, no subscription |
| The callback form | The side-effect subscription, no re-render |
defaultValue | The fallback before the form is mounted |
exact | The exact name matching |
compute | The derived value subscription |
Key takeaways:
- The two APIs read the same values, and the render boundary is the difference. The
watchre-renders the component that owns theuseFormcall, and theuseWatchre-renders only the component that calls the hook . The extraction of the value-display component is the mechanism that isolates the re-render. - The
useWatchis the performance choice for the nested rendering. Thecontrolprop is the connection between the form and the child, and the child re-renders only when the watched value changes . The form component does not re-render on the keystrokes. - The
getValuesis the no-subscription read. It reads the current values on demand, and it is the mechanism for the event handlers and the effects, not for the render . - The callback form is the side-effect subscription. The
watchcallback runs on every change without a re-render, and thetypeargument distinguishes the user input from the programmatic change . - The
computeoption reduces the re-render frequency. The function receives the watched value and returns the derived value, and the component re-renders only when the derived value changes . - The subscription timing matters. The
useWatchsubscribes when the component mounts, and a value set before the subscription is ignored. The custom hook that merges theuseWatchwith thegetValuesis the workaround .
Remember: The React Hook Form has two ways to read the values during the render, and the choice is the render scope. The watch is the form-level subscription, and the useWatch is the component-level subscription. The getValues is the no-subscription read, and the callback is the side-effect subscription. The compute is the derived-value subscription, and the exact is the name-matching option. The values are the same; the re-render boundary is the difference.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!