frontend / react / controlled_vs_uncontrolled.md

Controlled vs Uncontrolled Components

4 interview angles 11 min read source

Controlled vs Uncontrolled Components

Understanding the difference between controlled and uncontrolled components is crucial for React development, as it affects how you manage form data and component state.

Table of Contents


What are Controlled Components?

Controlled components are React components where the form data is handled by the component’s state and controlled by React.

Key Concepts:

  • State-driven: Form data is stored in React state
  • Single source of truth: React state controls the component
  • Predictable: Component behavior is fully controlled by React
  • Validation: Easy to implement real-time validation

Controlled Component Structure:

import React, { useState } from 'react';

function ControlledForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prevData => ({
      ...prevData,
      [name]: value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form submitted:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

What are Uncontrolled Components?

Uncontrolled components are React components where the form data is handled by the DOM itself, using refs to access the values.

Key Concepts:

  • DOM-driven: Form data is managed by the DOM
  • Refs: Use refs to access form values
  • Less code: Simpler implementation for basic forms
  • Performance: Fewer re-renders

Uncontrolled Component Structure:

import React, { useRef } from 'react';

function UncontrolledForm() {
  const nameRef = useRef();
  const emailRef = useRef();
  const messageRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();

    const formData = {
      name: nameRef.current.value,
      email: emailRef.current.value,
      message: messageRef.current.value
    };

    console.log('Form submitted:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        ref={nameRef}
        type="text"
        placeholder="Name"
      />
      <input
        ref={emailRef}
        type="email"
        placeholder="Email"
      />
      <textarea
        ref={messageRef}
        placeholder="Message"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Key Differences

1. State Management:

// Controlled: State manages form data
function ControlledInput() {
  const [value, setValue] = useState('');

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="Controlled input"
    />
  );
}

// Uncontrolled: DOM manages form data
function UncontrolledInput() {
  const inputRef = useRef();

  const handleSubmit = () => {
    console.log('Value:', inputRef.current.value);
  };

  return (
    <div>
      <input
        ref={inputRef}
        placeholder="Uncontrolled input"
      />
      <button onClick={handleSubmit}>Get Value</button>
    </div>
  );
}

2. Validation:

// Controlled: Real-time validation
function ControlledInputWithValidation() {
  const [value, setValue] = useState('');
  const [error, setError] = useState('');

  const handleChange = (e) => {
    const newValue = e.target.value;
    setValue(newValue);

    // Real-time validation
    if (newValue.length < 3) {
      setError('Value must be at least 3 characters');
    } else {
      setError('');
    }
  };

  return (
    <div>
      <input
        value={value}
        onChange={handleChange}
        placeholder="Enter value"
      />
      {error && <span style={{ color: 'red' }}>{error}</span>}
    </div>
  );
}

// Uncontrolled: Validation on submit
function UncontrolledInputWithValidation() {
  const inputRef = useRef();
  const [error, setError] = useState('');

  const handleSubmit = () => {
    const value = inputRef.current.value;

    if (value.length < 3) {
      setError('Value must be at least 3 characters');
    } else {
      setError('');
      console.log('Valid value:', value);
    }
  };

  return (
    <div>
      <input
        ref={inputRef}
        placeholder="Enter value"
      />
      <button onClick={handleSubmit}>Validate</button>
      {error && <span style={{ color: 'red' }}>{error}</span>}
    </div>
  );
}

3. Performance:

// Controlled: Re-renders on every keystroke
function ControlledPerformance() {
  const [value, setValue] = useState('');

  console.log('Controlled component re-rendered');

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="Type here..."
    />
  );
}

// Uncontrolled: No re-renders during typing
function UncontrolledPerformance() {
  const inputRef = useRef();

  console.log('Uncontrolled component re-rendered');

  return (
    <input
      ref={inputRef}
      placeholder="Type here..."
    />
  );
}

Implementation Examples

1. Controlled Form with Validation:

function ControlledFormWithValidation() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  });

  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const validateField = (name, value) => {
    switch (name) {
      case 'name':
        return value.length < 2 ? 'Name must be at least 2 characters' : '';

      case 'email':
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        return !emailRegex.test(value) ? 'Invalid email format' : '';

      case 'password':
        return value.length < 6 ? 'Password must be at least 6 characters' : '';

      case 'confirmPassword':
        return value !== formData.password ? 'Passwords do not match' : '';

      default:
        return '';
    }
  };

  const handleChange = (e) => {
    const { name, value } = e.target;

    setFormData(prev => ({
      ...prev,
      [name]: value
    }));

    // Validate field if it has been touched
    if (touched[name]) {
      const error = validateField(name, value);
      setErrors(prev => ({
        ...prev,
        [name]: error
      }));
    }
  };

  const handleBlur = (e) => {
    const { name, value } = e.target;

    setTouched(prev => ({
      ...prev,
      [name]: true
    }));

    const error = validateField(name, value);
    setErrors(prev => ({
      ...prev,
      [name]: error
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    // Validate all fields
    const newErrors = {};
    Object.keys(formData).forEach(key => {
      const error = validateField(key, formData[key]);
      if (error) {
        newErrors[key] = error;
      }
    });

    setErrors(newErrors);

    if (Object.keys(newErrors).length === 0) {
      console.log('Form is valid:', formData);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          name="name"
          value={formData.name}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Name"
        />
        {touched.name && errors.name && (
          <span style={{ color: 'red' }}>{errors.name}</span>
        )}
      </div>

      <div>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Email"
        />
        {touched.email && errors.email && (
          <span style={{ color: 'red' }}>{errors.email}</span>
        )}
      </div>

      <div>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Password"
        />
        {touched.password && errors.password && (
          <span style={{ color: 'red' }}>{errors.password}</span>
        )}
      </div>

      <div>
        <input
          type="password"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Confirm Password"
        />
        {touched.confirmPassword && errors.confirmPassword && (
          <span style={{ color: 'red' }}>{errors.confirmPassword}</span>
        )}
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}

2. Uncontrolled Form with File Upload:

function UncontrolledFileUpload() {
  const fileInputRef = useRef();
  const [selectedFiles, setSelectedFiles] = useState([]);

  const handleFileSelect = () => {
    const files = Array.from(fileInputRef.current.files);
    setSelectedFiles(files);
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    const formData = new FormData();
    selectedFiles.forEach(file => {
      formData.append('files', file);
    });

    // Submit files to server
    fetch('/api/upload', {
      method: 'POST',
      body: formData
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        ref={fileInputRef}
        type="file"
        multiple
        onChange={handleFileSelect}
        accept="image/*,.pdf,.doc,.docx"
      />

      {selectedFiles.length > 0 && (
        <div>
          <h4>Selected Files:</h4>
          <ul>
            {selectedFiles.map((file, index) => (
              <li key={index}>
                {file.name} ({(file.size / 1024).toFixed(2)} KB)
              </li>
            ))}
          </ul>
        </div>
      )}

      <button type="submit" disabled={selectedFiles.length === 0}>
        Upload Files
      </button>
    </form>
  );
}

3. Mixed Approach:

function MixedForm() {
  // Controlled state for form fields
  const [formData, setFormData] = useState({
    name: '',
    email: ''
  });

  // Uncontrolled refs for file inputs
  const fileInputRef = useRef();
  const hiddenInputRef = useRef();

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    // Get controlled form data
    const controlledData = { ...formData };

    // Get uncontrolled file data
    const files = fileInputRef.current.files;
    const hiddenValue = hiddenInputRef.current.value;

    const submitData = {
      ...controlledData,
      files: Array.from(files),
      hiddenValue
    };

    console.log('Submitting:', submitData);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Controlled inputs */}
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />

      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />

      {/* Uncontrolled inputs */}
      <input
        ref={fileInputRef}
        type="file"
        multiple
      />

      <input
        ref={hiddenInputRef}
        type="hidden"
        defaultValue="hidden-value"
      />

      <button type="submit">Submit</button>
    </form>
  );
}

Advanced Patterns

1. Controlled Component with Custom Hook:

function useForm(initialValues = {}) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({
      ...prev,
      [name]: true
    }));
  };

  const setFieldValue = (name, value) => {
    setValues(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const setFieldError = (name, error) => {
    setErrors(prev => ({
      ...prev,
      [name]: error
    }));
  };

  const reset = () => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  };

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    setFieldValue,
    setFieldError,
    reset
  };
}

// Usage
function FormWithCustomHook() {
  const {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    reset
  } = useForm({
    name: '',
    email: ''
  });

  return (
    <form>
      <input
        name="name"
        value={values.name}
        onChange={handleChange}
        onBlur={handleBlur}
        placeholder="Name"
      />
      {touched.name && errors.name && (
        <span style={{ color: 'red' }}>{errors.name}</span>
      )}

      <input
        name="email"
        value={values.email}
        onChange={handleChange}
        onBlur={handleBlur}
        placeholder="Email"
      />
      {touched.email && errors.email && (
        <span style={{ color: 'red' }}>{errors.email}</span>
      )}

      <button type="button" onClick={reset}>Reset</button>
    </form>
  );
}

2. Uncontrolled Component with Imperative Handle:

const UncontrolledInput = forwardRef((props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    blur: () => inputRef.current.blur(),
    getValue: () => inputRef.current.value,
    setValue: (value) => {
      inputRef.current.value = value;
    },
    select: () => inputRef.current.select()
  }));

  return <input ref={inputRef} {...props} />;
});

// Usage
function ParentComponent() {
  const inputRef = useRef();

  const handleFocus = () => {
    inputRef.current.focus();
  };

  const handleGetValue = () => {
    console.log('Input value:', inputRef.current.getValue());
  };

  const handleSetValue = () => {
    inputRef.current.setValue('New value');
  };

  return (
    <div>
      <UncontrolledInput ref={inputRef} placeholder="Type here..." />
      <button onClick={handleFocus}>Focus</button>
      <button onClick={handleGetValue}>Get Value</button>
      <button onClick={handleSetValue}>Set Value</button>
    </div>
  );
}

3. Dynamic Form Fields:

function DynamicForm() {
  const [fields, setFields] = useState([
    { id: 1, name: 'field1', value: '' }
  ]);

  const addField = () => {
    const newField = {
      id: Date.now(),
      name: `field${fields.length + 1}`,
      value: ''
    };
    setFields(prev => [...prev, newField]);
  };

  const removeField = (id) => {
    setFields(prev => prev.filter(field => field.id !== id));
  };

  const updateField = (id, value) => {
    setFields(prev => prev.map(field =>
      field.id === id ? { ...field, value } : field
    ));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = fields.reduce((acc, field) => {
      acc[field.name] = field.value;
      return acc;
    }, {});
    console.log('Form data:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      {fields.map(field => (
        <div key={field.id}>
          <input
            value={field.value}
            onChange={(e) => updateField(field.id, e.target.value)}
            placeholder={`Field ${field.name}`}
          />
          <button type="button" onClick={() => removeField(field.id)}>
            Remove
          </button>
        </div>
      ))}

      <button type="button" onClick={addField}>
        Add Field
      </button>

      <button type="submit">Submit</button>
    </form>
  );
}

Best Practices

1. Choose Based on Use Case:

// Use controlled for complex forms with validation
function ComplexForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: ''
  });

  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));

    // Real-time validation
    validateField(name, value);
  };

  return (
    <form>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
      />
      {errors.name && <span>{errors.name}</span>}
    </form>
  );
}

// Use uncontrolled for simple file uploads
function SimpleFileUpload() {
  const fileRef = useRef();

  const handleSubmit = () => {
    const files = fileRef.current.files;
    // Process files
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={fileRef} type="file" />
      <button type="submit">Upload</button>
    </form>
  );
}

2. Performance Considerations:

// Good: Use uncontrolled for performance-critical inputs
function PerformanceOptimizedForm() {
  const [criticalData, setCriticalData] = useState('');
  const nonCriticalRef = useRef();

  return (
    <form>
      {/* Controlled for critical data */}
      <input
        value={criticalData}
        onChange={(e) => setCriticalData(e.target.value)}
        placeholder="Critical data"
      />

      {/* Uncontrolled for non-critical data */}
      <input
        ref={nonCriticalRef}
        placeholder="Non-critical data"
      />
    </form>
  );
}

3. Validation Strategy:

// Good: Controlled for real-time validation
function ValidatedForm() {
  const [value, setValue] = useState('');
  const [error, setError] = useState('');

  const handleChange = (e) => {
    const newValue = e.target.value;
    setValue(newValue);

    // Real-time validation
    if (newValue.length < 3) {
      setError('Too short');
    } else {
      setError('');
    }
  };

  return (
    <div>
      <input value={value} onChange={handleChange} />
      {error && <span style={{ color: 'red' }}>{error}</span>}
    </div>
  );
}

Common Interview Questions

Q: What is the difference between controlled and uncontrolled components?

  • Controlled components manage form data through React state, while uncontrolled components use DOM refs to access form values.

Q: When should you use controlled components?

  • For complex forms with validation, real-time updates, or when you need to control the form state programmatically.

Q: When should you use uncontrolled components?

  • For simple forms, file uploads, or when you want better performance with minimal re-renders.

Q: What are the advantages of controlled components?

  • Predictable behavior, easy validation, and full control over form state.

Q: What are the advantages of uncontrolled components?

  • Better performance, less code, and easier integration with non-React code.

Q: How do you handle form validation in controlled components?

  • Use state to track errors and validate on change or blur events.

Q: How do you handle form validation in uncontrolled components?

  • Validate on form submission or use refs to access values when needed.

Q: Can you mix controlled and uncontrolled components?

  • Yes, you can use controlled components for some fields and uncontrolled for others in the same form.

Q: What’s the performance impact of controlled vs uncontrolled?

  • Controlled components re-render on every change, while uncontrolled components don’t re-render during typing.

Q: How do you convert between controlled and uncontrolled components?

  • Add/remove value prop and onChange handler for controlled, or add/remove ref for uncontrolled.

Summary

  • Controlled components use React state to manage form data
  • Uncontrolled components use DOM refs to access form values
  • Choose based on use case: controlled for complex forms, uncontrolled for simple inputs
  • Performance considerations: uncontrolled components have fewer re-renders
  • Validation strategies: controlled allows real-time validation, uncontrolled validates on submit
  • Best practices include choosing the right approach and considering performance needs
  • Understanding both patterns is essential for effective React form development

Interview angle

  • “Controlled or uncontrolled?” - controlled when React state is the source of truth (value plus onChange): needed for validation as you type, conditional formatting, or driving other UI from the field. Uncontrolled when the DOM owns the value and you read it on submit via a ref or FormData - simpler and faster for large forms.
  • “What causes the ‘changing an uncontrolled input to be controlled’ warning?” - the value started as undefined or null and later became a string. Initialise to "", not undefined.
  • “Why do large controlled forms get slow?” - every keystroke re-renders the subtree. Fixes: keep field state local to the field, use uncontrolled inputs with a form library, or debounce the derived work rather than the input itself.
  • “What changed in React 19?” - form Actions plus useActionState and useFormStatus handle submission, pending state and errors without a controlled-state scaffold, which removes much of the reason to control every field.