How to Control React Hook Form Fields
Wire controlled inputs into React Hook Form with register and Controller, so the form state stays in one place and re-renders stay bounded.
On this page
Introduction
The hardest part of manage controlled form state is not writing the code — it is deciding what the code is allowed to reject. A React Hook Form setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in React Hook Form, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.
The cost of manage controlled form state without React Hook Form is paid in incidents, not in code. The code looks fine because the happy path works, and the happy path is what you demo. The failure path is what you operate, and the failure path is where React Hook Form earns its keep. Instrument the rejections, alert on the spikes, and let the metric tell you when the guard is misaligned with reality.
The sections below walk through the concrete decisions: where to put the react hook form check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.
Register plain inputs
Register plain inputs is the first decision because it determines what the rest of the React Hook Form pipeline can assume. A wrong choice here turns into manage controlled form state under load, and the invariant you thought React Hook Form guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Register plain inputs is the one most teams skip: drive react hook form to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
const { register, control, handleSubmit } = useForm();
<input {...register('name', { required: true })} />
<Controller name="date" control={control}
render={({ field }) => <DatePicker {...field} />} />
The typescript above is the shape to ship for manage controlled form state. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.
Use Controller for custom
The mechanism behind Use Controller for custom is straightforward in React Hook Form, but the consequences ripple. A wrong choice here turns into manage controlled form state under load, and the invariant you thought React Hook Form guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Use Controller for custom is the one most teams skip: drive react hook form to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Watch vs useWatch
Watch vs useWatch is where most React Hook Form implementations leak performance. A wrong choice here turns into manage controlled form state under load, and the invariant you thought React Hook Form guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Watch vs useWatch is the one most teams skip: drive react hook form to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
test('submits controlled values', async () => {
await user.type(screen.getByRole('textbox'), 'Ada');
await user.click(screen.getByRole('button'));
expect(onSubmit).toHaveBeenCalledWith({ name: 'Ada' });
});
Pin the behavior with this test. A React Hook Form setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.
Set default values
The hard part of Set default values is not the happy path — it is what happens when react hook form fails halfway. A wrong choice here turns into manage controlled form state under load, and the invariant you thought React Hook Form guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Set default values is the one most teams skip: drive react hook form to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Reset on external change
Reset on external change should be observable without a debugger. A wrong choice here turns into manage controlled form state under load, and the invariant you thought React Hook Form guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Reset on external change is the one most teams skip: drive react hook form to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Test with useFormContext
Test with useFormContext is a trade, not a winner. A wrong choice here turns into manage controlled form state under load, and the invariant you thought React Hook Form guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test with useFormContext is the one most teams skip: drive react hook form to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Common pitfalls
The common pitfalls in manage controlled form state all reduce to one mistake: treating React Hook Form as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.
- Skipping the react hook form boundary so manage controlled form state enters unvalidated.
- Assuming React Hook Form is fast without measuring the tail latency under load.
- Letting manage controlled form state fail silently instead of failing loudly with a metric.
- Testing only the happy path so the react hook form failure surfaces in production.
- Sharing mutable state across requests so one bad input poisons the next.
Conclusion
Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: React Hook Form fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.
Self-check: write a test that drives react hook form to the failing condition and assert the system degrades gracefully. Then manage controlled form state is a property you maintain, not a feature you hope for.
Sources: React Hook Form, Controller
FAQ
- When use Controller over register?
- When the input is a custom component whose value is not a DOM string, or needs controlled props.
- How avoid re-renders?
- Use useWatch with shouldUnregister or move the watched field into a child component.
Report a problem with this articleOpens a pre-filled issue on GitHub.