Hey guys!
I kept exploring React Hook Form (RHF) over the past week, and something became very clear: it’s one of the most performant and thoughtful form libraries in the React ecosystem. After sharing a set of beginner-friendly lessons in my previous post, I’m back with more discoveries that will hopefully save you from some debugging headaches.
If you haven’t read the first article, you can find it here:
➡️ Building Forms Using React Hook Form #1
Now—on to the juicy bits.
More Lessons & Tips
1. useWatch() and re-renders
useWatch() lets you track field values without re-rendering the entire form, but only if you watch fields narrowly and intentionally. Watching a large object, an array, or a whole form node can cause your component to re-render far more often than needed.
Bad example — watching the entire array:
const fieldGroupsArray = useWatch({
name: "fieldGroupsArray", // ❌ triggers re-render whenever anything changes in the array
});
Better — watch only what you need:
const conditionalValue = useWatch({
name: "fieldGroupsArray.0.conditionalField", // ✅ only reacts to this field
});
Note that:
-
watch()re-renders the form where useForm() lives -
useWatch()re-renders only the component calling it
Use the second option for large form structures or repeated field groups. Your CPU will thank you.
2. Subscribing to formState can be expensive
Accessing formState is powerful, but it has consequences. Some values trigger re-renders more often than others:
-
dirtyFieldsupdates on every change while typing → constant re-renders -
touchedFieldsupdates when a user focuses on the fields -
errorschanges when validation runs. Not a problem unless your validation runs endlessly
Access only what you need. If you destructure everything because of "why not"—you might introduce performance issues blindly.
3. When using Zod with dynamic fields, be cautious with shouldUnregister
Like me, if you’re a user of Zod + RHF's dynamic conditional fields, think twice before enabling:
useForm({ shouldUnregister: true }) // ⚠️ Caution
Unregistered fields get completely removed from the form state, meaning you may lose default values that are still relevant for validation. This can lead to unexpected Zod validation failures, especially when showing/hiding fields conditionally.
The shouldUnregister flag immediately removes fields from RHF's internal state if it is not registered to any form input. In dynamic schemas, keeping fields registered helps preserve data consistency.
4. About using <Controller> / useController() when the standard register() isn't enough
Scenarios where it is suitable:
- External UI libraries with no direct
ref,value, oronChangeprops exposed. - Controlled inputs (e.g. Chakra UI
<Select />) which possess their own separate internal state - Complex custom components where you want granular control over which elements receive
{...field}
Example:
// You may bind only <input> to form state instead of rerendering label + error
<CustomField>
<label>Username</label>
<input {...field} />
<p role="alert">{errorMessage}</p>
</CustomField>
This reduces noise, unnecessary renders, and keeps the field logic predictable.
Wrapping Up
Not all of this may be new to you — maybe you nodded through every section, maybe some of it made you open your project and fix things immediately. Either way, if we both improved our understanding today, that’s already a win.
Feel free to drop suggestions, corrections, or even healthy criticism in the comments.
More experiments coming soon 😉
Top comments (0)