Controlled vs. Uncontrolled Inputs in React: Making the Practical Call
AI generated
{ }
React · Forms · Performance
Controlled vs. Uncontrolled Inputs in React
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.

14 min read Controlled inputs Uncontrolled inputs Form performance

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.

11. FAQ: Controlled vs. Uncontrolled Inputs: The Essentials at a Glance

1What is the fundamental difference between controlled and uncontrolled inputs?
With controlled fields, React holds the value in state through value and onChange, with uncontrolled fields the DOM holds the value itself, and React only accesses it on demand through a ref.
2Why are uncontrolled fields faster for large forms?
Because no React re-render happens while typing, the browser updates the field natively. With controlled fields, on the other hand, every keystroke triggers a state update and therefore a re-render of the holding component.
3Do I have to commit to one approach for the entire form?
No, in practice a hybrid approach is usually the most sensible: the majority of fields stay uncontrolled, only the few fields with a genuine need for live feedback get controlled or observed through useWatch().
4Why are file uploads always uncontrolled?
For security reasons, browsers do not allow the value of an input type file to be set programmatically through a value prop. The only possible access is through a ref on the field.
5What does the React warning about switching between controlled and uncontrolled mean?
It usually appears when the passed-in state value starts out as undefined or null and later receives a genuine value. The fix is to initialize the state from the start with a defined empty string.
6When do I definitely need a controlled field?
Whenever the current value is needed by another part of the interface while typing, for example a live character count, synchronous formatting of the input, or immediate validation with feedback.
7How does React Hook Form fit into this decision?
React Hook Form registers fields uncontrolled by default through refs with register(), but offers useWatch() as a targeted, isolated way to still observe individual fields' current values live, without losing the performance advantage.
8Can I switch a field from uncontrolled to controlled afterward?
Technically yes, React allows it but warns against it, since the value already entered can get lost or become inconsistent in the process. A clean switch is achieved more reliably through a full remount with a changed key value.
9Is the performance difference even relevant for small forms?
Barely, for three to five fields the extra re-render cost of controlled fields is usually not noticeable. The difference only becomes relevant for forms with many fields or expensively rendered child components.
10How can I observe the difference in my own application?
With the React DevTools option Highlight updates when components render, you can directly see whether a form flashes completely on every keystroke, controlled, or only reacts on submit, uncontrolled.