What Is the Difference Between Controlled and Uncontrolled Components?

Intermediate10 min interview
Skills tested:
Implementing a controlled input with value from state and onChange updating stateImplementing an uncontrolled input with defaultValue and a ref to read the value on submitKnowing what each approach means for re-renders: controlled triggers re-render on every keystroke, uncontrolled does notKnowing when controlled is better (live validation, dependent fields, preview) vs when uncontrolled is fine (submit-only forms)Understanding that react-hook-form uses uncontrolled inputs by default for performance

Advertisement

🧩 Scenario

Controlled vs uncontrolled is one of the most commonly asked React form interview questions. It is not purely academic: the performance difference matters in large forms, and understanding why react-hook-form is fast requires knowing that it uses uncontrolled inputs under the hood. A senior candidate should be able to choose between them confidently and explain the reasoning.

Architecture Walkthrough

Controlled Components

A controlled component is a form element whose value is set from React state and updated via an event handler. The cycle is: React state holds the value, renders it as the element's value prop, onChange fires when the user types, the handler calls the state setter, React re-renders with the new state, and the input displays the updated value. React is the single source of truth.

This approach has a performance cost: every keystroke triggers a state update and re-render of the component (and its children, unless memoized). For most forms this is imperceptible, but in very large forms with many concurrent fields it can become a bottleneck.

The main advantages of controlled components: the value is always available in state (no need to query the DOM), live validation and previews work naturally, dependent fields update in sync, and programmatic reset is trivial (just set the state).

Uncontrolled Components

An uncontrolled component lets the DOM own the value. Instead of value, you use defaultValue to set the initial value (the DOM updates freely after that). To read the value, you attach a ref and access ref.current.value on demand (typically in a submit handler). There is no re-render on every keystroke.

This approach is simpler for submit-only forms where you do not need the value during typing. It is also the strategy react-hook-form uses by default, which is why it is significantly faster than Formik for large forms. The cost is that the value is not in React state: live validation, dependent field updates, and previews require extra work.

File inputs (<input type="file">) must always be uncontrolled. React does not allow setting their value programmatically for security reasons.


Key Code Explained

// Controlled: React owns the value
import { useState } from 'react';

function ControlledForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Values are already in state — no DOM query needed
    console.log({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}                              // React state drives the input
        onChange={(e) => setEmail(e.target.value)} // state updates on every keystroke
        placeholder="Email"
      />
      {/* Live validation: possible because value is in state */}
      {email && !email.includes('@') && (
        <p className="error">Enter a valid email</p>
      )}

      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Log In</button>
    </form>
  );
}


// Uncontrolled: DOM owns the value
import { useRef } from 'react';

function UncontrolledForm() {
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Read from the DOM via refs — only needed on submit
    const email = emailRef.current?.value ?? '';
    const password = passwordRef.current?.value ?? '';
    console.log({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        ref={emailRef}
        type="email"
        defaultValue=""  // initial value only — DOM updates freely after this
        placeholder="Email"
      />
      <input
        ref={passwordRef}
        type="password"
        defaultValue=""
        placeholder="Password"
      />
      <button type="submit">Log In</button>
    </form>
  );
}


// Why controlled makes live features easy
function LiveSearchInput() {
  const [query, setQuery] = useState('');
  const suggestions = useSuggestions(query);  // value is available every keystroke

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {/* suggestions driven by state — trivial with controlled */}
      {suggestions.length > 0 && (
        <ul>
          {suggestions.map((s) => <li key={s.id}>{s.label}</li>)}
        </ul>
      )}
    </div>
  );
}


// File input: must always be uncontrolled
function FileUpload() {
  const fileRef = useRef<HTMLInputElement>(null);

  const handleUpload = () => {
    const file = fileRef.current?.files?.[0];
    if (file) uploadToServer(file);
  };

  return (
    <div>
      {/* No value prop — file inputs are always uncontrolled */}
      <input ref={fileRef} type="file" accept="image/*" />
      <button onClick={handleUpload}>Upload</button>
    </div>
  );
}

The LiveSearchInput example illustrates why controlled inputs are the default choice: the value is available in state on every keystroke, making the suggestions hook trivial to implement. With an uncontrolled input you would need a ref and an onChange to read the value into a local variable, at which point you have essentially rebuilt a controlled input without React's re-render guarantee.


Tradeoffs

AspectControlledUncontrolled
Value sourceReact stateDOM
Re-rendersEvery keystrokeOnly on submit/explicit read
Live validationTrivial (value in state)Requires extra plumbing
ResetSet state to initial valueCall reset() on the form or ref
Library adoptionFormik (controlled)react-hook-form (uncontrolled)
File inputsNot supportedRequired

What Interviewers Actually Check

  • Whether you can implement both patterns correctly
  • Whether you know the re-render cost difference
  • Whether you can choose the right pattern for the use case
  • Whether you know file inputs must be uncontrolled
  • Whether you know why react-hook-form is faster (uncontrolled by default)

Follow-Up Questions

  1. How does react-hook-form's register() API use uncontrolled inputs under the hood, and how does Controller wrap third-party controlled components?
  2. How would you reset an uncontrolled form to its default values after a successful submission?
  3. How does React's useId() hook help with associating labels and inputs correctly in form components?
  4. What is the performance cost of a controlled input in a 100-field form and how would you measure it with React DevTools Profiler?
  5. How does Formik's setFieldValue differ from a plain setState for controlled inputs?

Common Candidate Mistakes

  • Setting value without onChange and being surprised the input is read-only (React locked it)
  • Mixing value and defaultValue on the same input, which React warns about because they conflict
  • Not knowing that re-renders on every keystroke is a real cost in large controlled forms
  • Trying to use a controlled file input (<input type="file" value={...} />) and not knowing React explicitly disallows it
  • Reading ref.current.value inside a render function where it reflects the DOM state, not React state, causing subtle synchronization bugs

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you implement a controlled input with value from state and onChange updating state?
  • Can you implement an uncontrolled input with defaultValue and a ref to read the value on submit?
  • Can you explain the re-render difference between the two and when it matters?
  • Can you choose the right approach for a given form scenario?
  • Can you explain why file inputs must always be uncontrolled?

Summary

A controlled component stores its value in React state. The input renders the state value and calls a state setter on every onChange. React is the single source of truth, the value is always available in state, and live features like validation, dependent fields, and previews are trivial. The cost is a re-render on every keystroke.

An uncontrolled component lets the DOM own its value. The initial value is set with defaultValue and the DOM updates freely as the user types with no React re-renders. To read the value, attach a ref and access ref.current.value when needed (typically on submit). This is how react-hook-form achieves its performance advantage over Formik.

Default to controlled components. They are more predictable and easier to work with in any interactive scenario. Uncontrolled components are appropriate for submit-only forms with many fields where the keystroke re-render cost is measurable, or when integrating with third-party DOM libraries. File inputs must always be uncontrolled because React does not support setting their value programmatically.

Frequently Asked Questions

Which should I use by default — controlled or uncontrolled?

Default to controlled components. They make form state predictable, easy to validate, and easy to use in live previews or dependent fields. Uncontrolled components are appropriate for simple forms where you only need the values on submit and re-renders on every keystroke are undesirable.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement