Controlled vs Uncontrolled Components

Difficulty: Beginner

Question

What is the difference between controlled and uncontrolled components in React?

Answer

Alright, controlled vs uncontrolled components. This is one of those topics that sounds deceptively simple but comes up in interviews all the time, and the depth of your answer really separates juniors from mid-level developers. Let me break it down properly.

Think of it this way. You walk into a restaurant. In one scenario (controlled), the waiter takes your order, writes it down, and repeats it back to you after every item you mention. The waiter has a perfect record of your order at all times. In the other scenario (uncontrolled), you write your order on a piece of paper yourself, and the waiter only looks at it when you're done and hand it over. Both approaches get the order placed, but they work very differently.

A controlled component is a form element whose value is driven by React state. You set the input's value prop to a state variable, and you provide an onChange handler that updates that state. Every single character the user types flows through React: the user types a letter, onChange fires, you call setState with the new value, React re-renders, and the input displays the updated state. React is the single source of truth. The input literally cannot display anything that React doesn't know about.

An uncontrolled component, on the other hand, lets the browser's DOM handle the form data. Instead of binding the input to state, you use a ref to grab the value from the DOM when you need it (usually on form submission). The input manages its own internal state, just like a plain HTML input would. You can set an initial value with defaultValue, but after that, the DOM is in charge.

So why would you choose one over the other? Let's dig into the real trade-offs.

Controlled components give you superpowers: - Real-time validation. You can check the email format on every keystroke and show or hide error messages instantly. - Input masking and formatting. Want to automatically format a phone number as (123) 456-7890 while the user types? With controlled components, you can transform the value in your onChange handler before setting state. - Conditional logic. You can disable the submit button until all fields are valid, or show a password strength meter that updates in real time. - Synchronized inputs. Need two inputs to stay in sync, like a temperature converter that shows both Celsius and Fahrenheit? Controlled components make this natural because everything flows through state.

The downside of controlled components is more code. Every input needs a state variable and an onChange handler. For a form with 15 fields, that's a lot of boilerplate. Each keystroke triggers a re-render of the component, which in most cases is fast enough to be invisible, but in very large forms on slower devices it can theoretically become a concern.

Uncontrolled components are simpler for basic cases. No state management, no onChange handlers. The DOM just does its thing, and you read the values when you submit. This can be appealing for simple forms where you don't need real-time validation.

But there are cases where you MUST use uncontrolled components. The most common one is file inputs. The <input type="file"> element is always uncontrolled because its value is read-only for security reasons. You can't programmatically set which file is selected. You can only read it through a ref. Similarly, if you're integrating with a non-React library that manipulates the DOM directly (like a rich text editor or a jQuery plugin), you'll likely need uncontrolled patterns.

A really important gotcha: never switch between controlled and uncontrolled during a component's lifecycle. If you render an input with a value prop and then later render it without one (or vice versa), React will warn you. This happens when people accidentally pass undefined as the value, like when the state hasn't loaded yet: value={user?.name} will be undefined before the user loads, making the input uncontrolled, then controlled once the user loads. Fix this with value={user?.name || ''} to always provide a string.

In the real world, the overwhelming majority of React forms use controlled components. That's the React team's recommendation and the community standard. However, for large, performance-sensitive forms, many teams reach for React Hook Form, which is a library that brilliantly combines the best of both worlds. Under the hood, it uses uncontrolled inputs with refs for performance (no re-render on every keystroke), but it provides a controlled-like API with validation, error messages, and reactive form state. It's the most popular form library in the React ecosystem for good reason.

For interviews, make sure you can clearly articulate the definition (controlled = React state drives the value, uncontrolled = DOM holds the value), the trade-offs (control vs simplicity), and when each is appropriate. Bonus points if you mention React Hook Form as a modern solution that bridges the gap.

Code examples

Controlled Component

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

  // Validate on every change
  function handleEmailChange(e) {
    const value = e.target.value;
    setEmail(value);
    if (!value.includes('@')) {
      setErrors(prev => ({ ...prev, email: 'Invalid email' }));
    } else {
      setErrors(prev => ({ ...prev, email: null }));
    }
  }

  // Can enforce format
  function handlePasswordChange(e) {
    // Limit to 20 characters
    setPassword(e.target.value.slice(0, 20));
  }

  function handleSubmit(e) {
    e.preventDefault();
    // Values are already in state
    console.log({ email, password });
  }

  const isValid = email.includes('@') && password.length >= 8;

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={handleEmailChange} />
      {errors.email && <span>{errors.email}</span>}
      <input type="password" value={password} onChange={handlePasswordChange} />
      <button disabled={!isValid}>Submit</button>
    </form>
  );
}

React state drives the input values. Every keystroke triggers onChange which updates state, which re-renders the input with the new value. This enables real-time validation and format enforcement.

Uncontrolled Component

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

  function handleSubmit(e) {
    e.preventDefault();
    // Read values from DOM when needed
    const email = emailRef.current.value;
    const password = passwordRef.current.value;
    console.log({ email, password });
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* defaultValue sets initial value, DOM manages current value */}
      <input ref={emailRef} defaultValue="" />
      <input ref={passwordRef} type="password" defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

// File inputs are ALWAYS uncontrolled
function FileUpload() {
  const fileRef = useRef(null);

  function handleUpload() {
    const file = fileRef.current.files[0];
    console.log('Selected file:', file.name);
  }

  return (
    <div>
      <input type="file" ref={fileRef} />
      <button onClick={handleUpload}>Upload</button>
    </div>
  );
}

Uncontrolled components use refs to read DOM values on demand. Use defaultValue (not value) to set initial values. File inputs must always be uncontrolled.

Hybrid Approach with React Hook Form

// React Hook Form uses uncontrolled inputs with refs
// but provides controlled-like features
import { useForm } from 'react-hook-form';

function HybridForm() {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors, isValid }
  } = useForm({ mode: 'onChange' });

  // watch gives you reactive values without controlling every input
  const password = watch('password');

  function onSubmit(data) {
    console.log(data);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('email', {
          required: 'Email is required',
          pattern: { value: /^[^@]+@[^@]+$/, message: 'Invalid email' }
        })}
      />
      {errors.email && <span>{errors.email.message}</span>}

      <input
        type="password"
        {...register('password', {
          required: true,
          minLength: { value: 8, message: 'Min 8 characters' }
        })}
      />

      <input
        type="password"
        {...register('confirmPassword', {
          validate: value => value === password || 'Passwords must match'
        })}
      />

      <button disabled={!isValid}>Submit</button>
    </form>
  );
}

React Hook Form uses uncontrolled inputs internally for performance but provides validation, error handling, and reactive features similar to controlled components.

Key points

Concepts covered

Controlled Components, Uncontrolled Components, Forms, Refs