DEV Community

Maxim Logunov
Maxim Logunov

Posted on

Controlled vs. Uncontrolled Components in React: A Practical Guide

In React development, one of the fundamental decisions you'll face when working with forms and inputs is whether to use controlled or uncontrolled components. This choice impacts everything from state management to performance and code structure.

What Are Controlled Components?

Controlled components are React components where form data is handled by React state. The component's state serves as the "single source of truth" for the input value.

import { useState } from 'react';

function ControlledForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ name, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <button type="submit">Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, React completely controls the input values through state variables (name and email). Every keystroke triggers a state update and re-render.

What Are Uncontrolled Components?

Uncontrolled components are components where form data is handled by the DOM itself. You use refs to access DOM values when needed, rather than storing them in React state.

import { useRef } from 'react';

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

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      name: nameRef.current.value,
      email: emailRef.current.value
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={nameRef}
        placeholder="Name"
      />
      <input
        type="email"
        ref={emailRef}
        placeholder="Email"
      />
      <button type="submit">Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, React doesn't track the input values—the DOM does. We only access the values when the form is submitted.

Key Differences at a Glance

Aspect Controlled Components Uncontrolled Components
Data Flow React state → DOM DOM → React (via refs)
Value Source React state DOM
Updates On every change On demand (form submit, etc.)
Validation Real-time validation Typically on submit
Re-renders Every keystroke Minimal
Complexity More boilerplate Less boilerplate

When to Use Controlled Components

1. Immediate Validation and Feedback

When you need to validate user input as they type (e.g., password strength meter, email format checking).

function PasswordInput() {
  const [password, setPassword] = useState('');
  const [strength, setStrength] = useState('');

  const evaluateStrength = (value) => {
    if (value.length < 5) return 'Weak';
    if (value.length < 10) return 'Medium';
    return 'Strong';
  };

  const handleChange = (e) => {
    const value = e.target.value;
    setPassword(value);
    setStrength(evaluateStrength(value));
  };

  return (
    <div>
      <input
        type="password"
        value={password}
        onChange={handleChange}
      />
      <span>Strength: {strength}</span>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Conditional Form Logic

When inputs depend on each other's values.

function TravelForm() {
  const [tripType, setTripType] = useState('one-way');
  const [returnDate, setReturnDate] = useState('');

  return (
    <form>
      <select value={tripType} onChange={(e) => setTripType(e.target.value)}>
        <option value="one-way">One-way</option>
        <option value="round-trip">Round-trip</option>
      </select>

      {/* Only show return date for round trips */}
      {tripType === 'round-trip' && (
        <input
          type="date"
          value={returnDate}
          onChange={(e) => setReturnDate(e.target.value)}
        />
      )}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Complex Form State Management

When using form libraries like Formik or React Hook Form, which internally use controlled patterns for advanced features.

4. Dynamic Form Generation

When form fields are generated based on external data or user actions.

When to Use Uncontrolled Components

1. Performance-Critical Forms

When dealing with large forms where re-rendering on every keystroke would cause performance issues.

function LargeUncontrolledForm() {
  const formRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(formRef.current);
    const data = Object.fromEntries(formData);
    // Process all form data at once
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      {/* 50+ inputs here */}
      <input name="field1" defaultValue="" />
      <input name="field2" defaultValue="" />
      {/* ... */}
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. File Inputs

File inputs are always uncontrolled in React since their value is read-only.

function FileUpload() {
  const fileRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Selected file:', fileRef.current.files[0]);
  };

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

3. Simple Forms with No Validation

When you only need to collect data on submit without any intermediate processing.

4. Integrating with Non-React Libraries

When you need to integrate with third-party libraries that manage their own DOM state.

Hybrid Approach: The Best of Both Worlds

In practice, many applications use a combination of both approaches:

function HybridForm() {
  // Controlled for important fields
  const [email, setEmail] = useState('');

  // Uncontrolled for less important fields
  const phoneRef = useRef();
  const addressRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    const data = {
      email, // From state
      phone: phoneRef.current.value, // From ref
      address: addressRef.current.value
    };
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Controlled: Need validation */}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />

      {/* Uncontrolled: Simple data collection */}
      <input
        type="tel"
        ref={phoneRef}
        defaultValue=""
      />
      <textarea
        ref={addressRef}
        defaultValue=""
      />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Modern Solutions: Form Libraries

Modern React form libraries often abstract these decisions:

  • React Hook Form: Primarily uses uncontrolled components with refs for performance, but supports controlled components when needed.
  • Formik: Primarily uses controlled components for comprehensive state management.
  • Final Form: Uses a hybrid approach with subscription-based updates.

Best Practices and Recommendations

  1. Default to controlled components for most form scenarios—they make your UI more predictable and easier to debug.

  2. Consider uncontrolled components when:

    • You have performance issues with complex forms
    • You're integrating with non-React code
    • You're building simple, submission-only forms
  3. Use the right tool for the job:

    • For login/registration forms: Controlled (immediate validation)
    • For search inputs: Controlled (live search)
    • For settings/preference forms: Either, depending on complexity
    • For file uploads: Uncontrolled (required)
  4. Remember accessibility: Both approaches can be accessible, but controlled components make it easier to implement real-time ARIA announcements and error messages.

Conclusion

The choice between controlled and uncontrolled components isn't about which is "better," but which is more appropriate for your specific use case. Controlled components give you more power and predictability, while uncontrolled components offer better performance and simpler code for certain scenarios.

As a general rule: Start with controlled components unless you have a specific reason not to. As your application grows, you can optimize with uncontrolled components where it makes sense, or leverage modern form libraries that handle these concerns for you.

Understanding both patterns makes you a more versatile React developer, capable of choosing the right approach for each situation and building more efficient, maintainable applications.

Top comments (0)