This week, I saw three different developers struggle with the same issue: using react-hook-form as if it were just another useState
wrapper. The library is one of the most efficient tools for handling forms in React SPAs — but only if we embrace its idiomatic patterns. Misusing it can quickly turn a simple form into a complex, brittle mess.
Let’s go through some of the most common mistakes I’ve seen, and how to fix them.
Mistake #1 — Ignoring register
and Controller
Some developers avoid register
or Controller
entirely. Instead, they do something like this:
1const { watch, setValue } = useForm();
2
3<input
4 value={watch("email")}
5 onChange={(e) => setValue("email", e.target.value)}
6/>
This is essentially re-implementing controlled components with useState. It defeats the purpose of react-hook-form, which was designed to keep inputs uncontrolled and lightweight, reducing re-renders.
✅ The idiomatic way:
1const { register } = useForm();
2
3<input type="email" {...register("email")} />
Or, when working with complex components (like Select or DatePicker):
1const { control } = useForm();
2
3<Controller
4 name="date"
5 control={control}
6 render={({ field }) => (
7 <DatePicker
8 ref={field.ref}
9 selected={field.value}
10 onChange={(date: Date, strDate: string) => field.onChange(strDate)}
11 />
12 )}
13/>
Mistake #2 — Manually passing form state down the tree
Another anti-pattern I’ve seen: using watch
at the top level and passing props down manually, or even duplicating register
calls in child components.
This makes forms verbose and error-prone. It also unnecessary more re-renders. Instead, use FormContext:
1const methods = useForm();
2
3<FormProvider {...methods}>
4 <FormChild />
5</FormProvider>
And in the child:
1const { register } = useFormContext();
2
3<input {...register("username")} />
This way, you keep form logic centralized, and every component can access the same form state without prop-drilling.
Mistake #3 — Overcomplicating default values
Many people struggle to preload form data. I’ve seen setups with multiple useEffect
calls just to populate fields:
1useEffect(() => {
2 setValue("name", user.name);
3 setValue("email", user.email);
4}, [user]);
While this works, it’s noisy and mixes concerns. A more idiomatic approach is to keep the form logic focused only on form handling, not data fetching or loading state.
✅ Cleaner pattern: wrap the form in a parent component that handles async data:
1// UserFormWrapper.tsx
2export function UserFormWrapper() {
3 const { data: user, isLoading } = useUserQuery();
4
5 if (isLoading) return <div>Loading...</div>;
6 if (!user) return <div>No data found</div>;
7
8 return <UserForm initialData={user} />;
9}
Inside the form, you can directly use those props to initialize defaultValues:
1// UserForm.tsx
2export function UserForm({ initialData }) {
3 const { register } = useForm({
4 defaultValues: {
5 name: initialData.name,
6 email: initialData.email,
7 },
8 });
9
10 return (
11 <form>
12 <input {...register("name")} />
13 <input {...register("email")} />
14 </form>
15 );
16}
This way:
- The wrapper deals with async fetching, loading, and error states.
- The form is always initialized with complete data, without juggling
useEffect
orreset
.
It makes the component tree more declarative: “only render the form when I have data.”
Other common traps
-
Mixing uncontrolled and controlled inputs: either commit to
register
/Controller
or you’ll risk inconsistent values. -
Forgetting validation rules: RHF integrates naturally with schema validators like Zod. Don’t reinvent validation logic inside
onSubmit
. -
Unnecessary re-renders: calling
watch()
at the top level on large forms without selectors will cause performance issues. PreferuseWatch
for granular subscriptions.
Final thoughts
React Hook Form is a zero-boilerplate, zero-cost abstraction when used properly. But treating it like a glorified useState
hook negates its benefits. If you lean into its idioms — register
, Controller
, FormProvider
, defaultValues
, and schema validation — your forms become simpler, faster, and more maintainable.