Making the practical call between value plus onChange and ref plus defaultValue
Few decisions in React look as small at the start and end up having as much influence on performance and code structure as the choice between controlled and uncontrolled input fields. We clarify the difference with concrete code, show the performance implications for forms with very many fields, and present hybrid approaches that combine both worlds deliberately.
Table of Contents
- 1. Understanding the basic distinction
- 2. What this means concretely for rendering
- 3. The difference with very many form fields
- 4. When controlled fields are clearly the right choice
- 5. When uncontrolled fields are clearly the right choice
- 6. Making the performance implications measurable
- 7. Hybrid approaches that combine both worlds
- 8. Common pitfalls when switching between both approaches
- 9. Making the decision based on concrete criteria
- 10. Summary
- 11. FAQ
1. Understanding the basic distinction
A controlled input field gets its current value entirely from React state, passed in through the value prop, and reports every change back to the component holding that state through onChange. React is thereby the sole source of truth for the field's content, the DOM element itself holds no independent state of its own. Every keystroke from the user triggers a state update through onChange, which in turn triggers a re-render that hands the value prop with the new value back to the input field.
An uncontrolled input field, on the other hand, manages its value itself in the DOM, the way classic HTML does anyway, and React only accesses it on demand through a ref, for example when the form is submitted. A defaultValue only sets the initial value, after that the browser handles every further keystroke on its own, with React not even being aware of it. This exact absence of React state on every keystroke is the core of the performance difference between the two approaches.
// Controlled: React holds the value in state
function ControlledInput() {
const [name, setName] = useState("");
return (
<input value={name} onChange={(e) => setName(e.target.value)} />
);
}
// Uncontrolled: the DOM holds the value, React reads via ref
function UncontrolledInput() {
const nameRef = useRef(null);
function handleSubmit() {
console.log(nameRef.current.value);
}
return <input ref={nameRef} defaultValue="" />;
}
2. What this means concretely for rendering
For a controlled field, literally every keystroke triggers a state-update re-render of the component holding the state, and of every one of its non-memoized child components. For a single field inside a small component that is trivially cheap, React is built exactly for this use case and usually renders such updates within a few milliseconds. The problem only appears through scale, not through the controlled approach itself.
For an uncontrolled field, on the other hand, no React re-render happens at all while typing, the browser updates the input field natively and very efficiently, exactly as it would for a form with no JavaScript at all. React only learns of the current value the moment it is explicitly accessed through the ref. This complete decoupling from the render cycle is the actual performance advantage, not a smaller amount of code or fewer lines compared to the controlled approach.
3. The difference with very many form fields
For a form with thirty, fifty, or more controlled fields inside the same component, the problem compounds: every single keystroke into any one field triggers a state update that re-renders the entire form component, including every one of the other twenty-nine or more fields, even if their content did not change at all. With expensively rendered fields, for example with validation icons, tooltips, or conditional display, this can show up as noticeable typing lag under fast input.
Uncontrolled fields sidestep this problem structurally, because no React re-render happens at all while typing, regardless of how many fields exist in the form. This is exactly why libraries like React Hook Form default to uncontrolled fields with ref registration: for large, production forms this approach scales noticeably better than a naive state-based setup with one useState per field or a single shared state object for the entire form.
4. When controlled fields are clearly the right choice
Controlled fields are indispensable as soon as a field's current value is needed by another part of the interface in real time, for example a live character-count display, a synchronously updated preview, or a validation that gives the user immediate feedback while typing instead of only when the field loses focus. For fields whose input needs to be actively formatted while typing too, for example automatic thousands separators for numbers or phone number formatting, a controlled field is the natural choice, since React can return the formatted value directly.
Small forms with few fields, where simplicity and direct readability of the code matter more than the last millisecond of render performance, also benefit from the controlled approach, since the current state is directly readable from state at any time, without having to be routed through a ref. For tests and Storybook stories, a controlled field is also easier to simulate, since its state can be explicitly set from the outside through props.
function CharacterCountedInput({ maxLength = 280 }) {
const [text, setText] = useState("");
return (
<div>
<textarea
value={text}
onChange={(e) => setText(e.target.value.slice(0, maxLength))}
/>
<p>{text.length} / {maxLength} characters</p>
</div>
);
}
5. When uncontrolled fields are clearly the right choice
Uncontrolled fields are excellent whenever a field's value is only needed on form submission and is not relevant to any other part of the interface while typing, which in practice applies to the large majority of all form fields. File uploads through input type="file" are practically always uncontrolled, since browsers do not accept the selected file value through a value prop programmatically anyway, for security reasons.
For very large, production forms too, for example an extensive checkout process with dozens of fields for billing and shipping address, the uncontrolled approach, usually mediated through a library like React Hook Form, is the more pragmatic choice, since its render cost stays constantly low regardless of field count. Only for the few fields that genuinely need live feedback does one then deliberately reach for a controlled variant or, in React Hook Form, for useWatch().
function CheckoutForm() {
const formRef = useRef(null);
function handleSubmit(e) {
e.preventDefault();
const formData = new FormData(formRef.current);
console.log(Object.fromEntries(formData));
}
return (
<form ref={formRef} onSubmit={handleSubmit}>
<input name="firstName" defaultValue="" />
<input name="lastName" defaultValue="" />
<input name="street" defaultValue="" />
<input name="city" defaultValue="" />
{/* Dozens more fields with zero render overhead */}
<button type="submit">Complete order</button>
</form>
);
}
6. Making the performance implications measurable
The difference can be made visible with the same tools that help with other re-render problems: with the React DevTools option Highlight updates when components render, a large, fully controlled form flashes its entire form component on every keystroke, while an uncontrolled variant shows no flashing at all while typing, since React is not involved at all. Only on submit, when the value gets read through the ref, does React activity become visible again.
For solid numbers, a short profiler recording during a typical, fast typing sequence in a larger form is worth adding. The measured difference is often smaller than expected on modern devices and small forms, React is well optimized for state updates, but it grows increasingly noticeable as field count rises and child components become more expensive, and past a certain point it becomes noticeable to end users too.
7. Hybrid approaches that combine both worlds
In practice, the decision is rarely binary for the entire form. A pragmatic hybrid approach leaves the majority of fields uncontrolled through refs and deliberately lifts only the few fields that genuinely need live feedback into controlled state, for example a password strength indicator or a character count. That keeps most of the form render-free while the few spots with a genuine need still get full access to the current value while typing.
In React Hook Form, that corresponds exactly to combining register() for the majority of fields with useWatch() inside an isolated subcomponent for the few fields with a live requirement, as already described in the comparison between watch() and useWatch(). This pattern, uncontrolled by default and controlled only selectively where it adds genuine value, delivers the best ratio between performance and developer friendliness in practice almost every time.
function SignupForm() {
const { register, control } = useForm();
return (
<form>
{/* Uncontrolled: zero render overhead */}
<input {...register("email")} />
<input {...register("company")} />
{/* Selectively controlled: for live feedback */}
<PasswordStrengthField control={control} />
</form>
);
}
function PasswordStrengthField({ control }) {
const password = useWatch({ control, name: "password" });
const strength = calculateStrength(password);
return (
<div>
<input {...control.register("password")} type="password" />
<StrengthMeter value={strength} />
</div>
);
}
8. Common pitfalls when switching between both approaches
A frequent mistake is unintentionally mixing both approaches on the same field: an input that receives both a value prop and a defaultValue, or a value prop without an accompanying onChange, produces a React warning about a field switching between controlled and uncontrolled, usually because the state starts out as undefined or null and only later receives a genuine value. The reliable fix is to initialize the state from the very start with a defined empty string instead of undefined.
A second pitfall involves switching an already mounted field from uncontrolled to controlled, for example when a field initially managed with a ref later receives a value prop. React technically allows this switch but rightfully warns against it, since the value already entered by the user in the DOM can get lost or become inconsistent with the new state value in the process. Anyone wanting to switch between the two modes should instead have the field fully remount through a key change.
9. Making the decision based on concrete criteria
The most practical approach does not start with a general preference for one approach or the other, it starts with a simple question per field: does any other part of the interface need this field's current value while typing, or is it enough to know it only on submit? In the first case a controlled field or useWatch() is the right choice, in the second case an uncontrolled field with a ref or register().
This field-by-field decision, rather than a blanket choice for the entire form, delivers the best result in practice almost every time: maximum performance through predominantly uncontrolled fields, combined with deliberate, controlled access exactly where it delivers genuine functional value. The table below sums up the most important criteria for this decision once more.
| Criterion | Controlled | Uncontrolled | Recommendation |
|---|---|---|---|
| Re-render per keystroke | Yes, the entire holding component | No, the browser updates natively | Prefer uncontrolled for many fields |
| Live feedback while typing | Directly available from state | Only after an explicit ref access | Use controlled for character counts or formatting |
| File uploads | Not possible, value not supported | The only viable approach | Always uncontrolled with a ref |
| Large forms, many fields | Scales poorly without isolation | Scales independently of field count | Uncontrolled or a library like React Hook Form |
Mironsoft
React architecture, performance, and Magento frontend integration
React frontends that stay fast instead of slowing down with every feature?
We review existing React applications for unnecessary re-renders, bloated bundles, and fragile state management, then build a frontend that stays performant and connects cleanly to Magento or other backends.
Performance Audit
Systematically measuring and fixing re-renders, bundle size, and load times.
State Architecture
Cleanly separating context, client state, and server state instead of mixing everything.
Magento Integration
Building robust, type-safe GraphQL or REST integration with Magento.
10. Summary
Controlled vs. Uncontrolled Inputs: The Essentials at a Glance
Controlled
value plus onChange, React holds the value in state, every keystroke triggers a re-render.
Uncontrolled
ref plus defaultValue, the browser holds the value natively in the DOM, no re-render while typing.
Rule of thumb
Only control what is genuinely needed live, leave the rest uncontrolled.
Hybrid approach
Majority of fields uncontrolled, individual fields with genuine live needs controlled selectively.