Controlled vs Uncontrolled Components

Difficulty: Intermediate

The choice between controlled and uncontrolled components is one of the most common React design decisions. Controlled components bind form values to React state, giving you full control over the data at every moment. Uncontrolled components let the DOM manage form values, and you read them only when needed (typically on submit). Each approach has clear trade-offs in terms of control, simplicity, and performance.

Controlled components are the recommended default for most React applications. They enable real-time validation (validate on every keystroke), input transformation (force formatting), conditional field behavior (enable/disable fields based on other values), and programmatic value changes (reset, populate from API). The trade-off is verbosity - every input needs state and an onChange handler - and performance - every keystroke triggers a re-render of the component.

Uncontrolled components are simpler for basic forms. They require less code (just a ref and defaultValue), don't cause re-renders on input, and integrate more naturally with non-React code. They're the right choice for file inputs (always uncontrolled), simple forms with no validation requirements, forms in performance-critical contexts, and integration with third-party DOM libraries.

Performance differences are usually negligible for small to medium forms. React's rendering is fast, and a re-render on every keystroke is typically imperceptible. However, for forms with many fields (50+) or components with expensive rendering logic, the difference can be noticeable. In such cases, uncontrolled components or debounced updates can help. React's concurrent features and automatic batching in React 18+ further reduce the performance impact of controlled components.

A practical rule of thumb: start with controlled components. Switch to uncontrolled only when you have a specific reason - file inputs, third-party library integration, measured performance problems, or very simple forms where the control is unnecessary. In an interview, demonstrate that you understand both patterns, their trade-offs, and when to choose each.

Code examples

Side-by-side comparison

// CONTROLLED: React state owns the value
function ControlledExample() {
  const [value, setValue] = React.useState('initial');
  console.log('[Controlled] Current value:', value);
  console.log('[Controlled] Re-renders on every change: yes');
  console.log('[Controlled] Can validate on change: yes');
  console.log('[Controlled] Can transform input: yes');
  return null;
}

// UNCONTROLLED: DOM owns the value
function UncontrolledExample() {
  const ref = React.useRef(null);
  ref.current = { value: 'initial' };
  console.log('[Uncontrolled] Current value:', ref.current.value);
  console.log('[Uncontrolled] Re-renders on every change: no');
  console.log('[Uncontrolled] Can validate on change: no');
  console.log('[Uncontrolled] Can transform input: no');
  return null;
}

ControlledExample();
UncontrolledExample();

Controlled components trade simplicity for control. Uncontrolled components trade control for simplicity. The choice depends on your form's requirements.

When to use each approach

function DecisionGuide() {
  const scenarios = [
    { need: 'Real-time validation', approach: 'Controlled' },
    { need: 'Input formatting/masking', approach: 'Controlled' },
    { need: 'Conditional field logic', approach: 'Controlled' },
    { need: 'Simple submit-only form', approach: 'Uncontrolled' },
    { need: 'File upload input', approach: 'Uncontrolled' },
    { need: 'Third-party DOM library', approach: 'Uncontrolled' }
  ];

  scenarios.forEach(s => {
    console.log(s.need + ' -> ' + s.approach);
  });

  return null;
}

DecisionGuide();

The decision depends on what features you need. If you need real-time feedback or input control, use controlled. If you just need the value on submit, uncontrolled is simpler.

Feature comparison table

const features = {
  'Value access': { controlled: 'Anytime (from state)', uncontrolled: 'On demand (from ref)' },
  'Validation timing': { controlled: 'onChange, onBlur, onSubmit', uncontrolled: 'onSubmit only' },
  'Re-renders': { controlled: 'Every input change', uncontrolled: 'None from input' },
  'Initial value': { controlled: 'useState(initial)', uncontrolled: 'defaultValue prop' },
  'Programmatic update': { controlled: 'setState(newValue)', uncontrolled: 'ref.current.value = x' }
};

Object.entries(features).forEach(([feature, comparison]) => {
  console.log(feature + ':');
  console.log('  Controlled: ' + comparison.controlled);
  console.log('  Uncontrolled: ' + comparison.uncontrolled);
});

This comparison highlights the fundamental differences. Controlled gives more power at the cost of more code and re-renders. Uncontrolled is simpler but limited.

Key points

Concepts covered

controlled vs uncontrolled, trade-offs, performance, use cases, form patterns