useWatch and Reactive Fields
React Hook Form minimizes re-renders by default — fields update the DOM directly through refs, and your component only re-renders on submit or when you read from formState. But sometimes you need to react to field values as the user types. Maybe you're building a live preview, showing conditional fields, or computing a derived value. That's where useWatch comes in.
watch vs useWatch
Before diving into useWatch, it helps to understand why watch isn't always the right tool. The watch method lives on the form instance returned by useForm:
const { register, watch } = useForm();
const firstName = watch("firstName");
This works, but there's a cost. Calling watch in a component subscribes that component to every field change in the form. Even if you only pass "firstName", the component re-renders when any field updates. For a small form at the top level, that's fine. In a deeply nested child component, it's wasteful.
useWatch solves this. It's a standalone hook that subscribes only to the fields you specify. The component that calls it re-renders only when those specific values change — nothing else.
import { useWatch } from "react-hook-form";
const firstName = useWatch({ control, name: "firstName" });
The key difference: useWatch needs the control object from useForm, and it isolates re-renders to the watched fields. Use watch when you're in the same component as useForm and don't care about extra renders. Use useWatch in child components where performance matters.
Basic Usage
useWatch takes an options object with control (from useForm) and name (the field or fields to watch). It returns the current value:
function GreetingPreview({ control }: { control: Control<FormValues> }) {
const firstName = useWatch({ control, name: "firstName" });
return <p>Hello, {firstName || "stranger"}!</p>;
}
This component re-renders only when firstName changes. The parent form component — which holds all the inputs — doesn't re-render at all.
Watching Multiple Fields
Pass an array of field names to watch several values at once. The return value is a tuple matching the order of the names:
function FullNamePreview({ control }: { control: Control<FormValues> }) {
const [firstName, lastName] = useWatch({
control,
name: ["firstName", "lastName"],
});
return (
<p>
{firstName} {lastName}
</p>
);
}
The component re-renders when either firstName or lastName changes, but not when unrelated fields like email or age update.
Use Case: Conditional Fields
A common pattern is showing or hiding fields based on another field's value. Here's a select that reveals an extra text input when the user picks "other":
function CategoryField({ control, register }: {
control: Control<FormValues>;
register: UseFormRegister<FormValues>;
}) {
const category = useWatch({ control, name: "category" });
return (
<div>
<label htmlFor="category">Category</label>
<select id="category" {...register("category")}>
<option value="bug">Bug</option>
<option value="feature">Feature</option>
<option value="other">Other</option>
</select>
{category === "other" && (
<div>
<label htmlFor="customCategory">Please specify</label>
<input id="customCategory" {...register("customCategory")} />
</div>
)}
</div>
);
}
Only this component re-renders when category changes. The rest of the form stays untouched.
Use Case: Computed Values
When one value depends on others — like a total price computed from quantity and unit price — useWatch keeps the calculation reactive without re-rendering the entire form:
function TotalPrice({ control }: { control: Control<FormValues> }) {
const [quantity, unitPrice] = useWatch({
control,
name: ["quantity", "unitPrice"],
});
const total = (Number(quantity) || 0) * (Number(unitPrice) || 0);
return <p>Total: ${total.toFixed(2)}</p>;
}
The parent form holds the quantity and unit price inputs. This child component subscribes to just those two fields and displays the computed total. No wasted renders from other fields changing.
Complete Example: Live Preview Form
Let's put it all together. This form collects event details and shows a live preview panel that updates as the user types. The preview is a separate component that uses useWatch to subscribe to all the fields it needs:
import { useForm, useWatch, Control } from "react-hook-form";
type EventFormValues = {
eventName: string;
date: string;
location: string;
ticketPrice: string;
ticketQuantity: string;
description: string;
};
function EventPreview({ control }: { control: Control<EventFormValues> }) {
const [name, date, location, price, quantity, description] = useWatch({
control,
name: [
"eventName",
"date",
"location",
"ticketPrice",
"ticketQuantity",
"description",
],
});
const total = (Number(price) || 0) * (Number(quantity) || 0);
return (
<div style={{ border: "1px solid #ccc", padding: 16, borderRadius: 8 }}>
<h2>{name || "Untitled Event"}</h2>
<p>{date || "No date set"} — {location || "No location"}</p>
<p>{description || "No description yet."}</p>
<hr />
<p>
{quantity || 0} tickets x ${Number(price || 0).toFixed(2)} ={" "}
<strong>${total.toFixed(2)}</strong>
</p>
</div>
);
}
function EventForm() {
const {
register,
handleSubmit,
control,
formState: { isSubmitting },
} = useForm<EventFormValues>({
defaultValues: {
eventName: "",
date: "",
location: "",
ticketPrice: "",
ticketQuantity: "",
description: "",
},
});
const onSubmit = async (data: EventFormValues) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("Event created:", data);
};
return (
<div style={{ display: "flex", gap: 32 }}>
<form onSubmit={handleSubmit(onSubmit)} style={{ flex: 1 }}>
<div>
<label htmlFor="eventName">Event Name</label>
<input id="eventName" {...register("eventName")} />
</div>
<div>
<label htmlFor="date">Date</label>
<input id="date" type="date" {...register("date")} />
</div>
<div>
<label htmlFor="location">Location</label>
<input id="location" {...register("location")} />
</div>
<div>
<label htmlFor="ticketPrice">Ticket Price ($)</label>
<input
id="ticketPrice"
type="number"
step="0.01"
{...register("ticketPrice")}
/>
</div>
<div>
<label htmlFor="ticketQuantity">Ticket Quantity</label>
<input
id="ticketQuantity"
type="number"
{...register("ticketQuantity")}
/>
</div>
<div>
<label htmlFor="description">Description</label>
<textarea id="description" rows={4} {...register("description")} />
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Creating..." : "Create Event"}
</button>
</form>
<div style={{ flex: 1 }}>
<h3>Preview</h3>
<EventPreview control={control} />
</div>
</div>
);
}
export default EventForm;
The EventForm component owns the form and all the inputs. It passes control down to EventPreview, which uses useWatch to subscribe to every field it needs for the preview. As the user types into any field, only EventPreview re-renders — the form component itself stays stable. The inputs update through refs, and the preview updates through useWatch. Each piece does its job without causing unnecessary work in the other.
This is the core pattern for reactive UIs with React Hook Form: keep the form in one component, put the reactive display logic in a child, and connect them with control and useWatch. The form stays fast because it never re-renders. The preview stays current because useWatch delivers targeted updates. You get reactivity where you need it and performance where you don't.