React Controlled vs Uncontrolled Components

I’ve seen developers struggle most with one specific area: handling form data. It seems simple at first, but choosing the wrong approach can lead to buggy interfaces and difficult-to-maintain codebases.

I remember working on a large-scale fintech dashboard for a New York-based firm where we had hundreds of input fields.

We had to decide between controlled and uncontrolled components, and that choice changed everything about our performance and state management.

In this tutorial, I’ll walk you through exactly how these two patterns work and when you should use each one in your professional projects.

What are Controlled Components in React?

A controlled component is a component where React is the “single source of truth” for the form data.

Instead of the DOM holding the form data, the React state handles it. Whenever the input changes, we update the state.

Method 1: Use the useState Hook for Controlled Inputs

This is the most common way I handle forms in modern React applications. It gives you total control over the input values.

Imagine we are building a simple “Tax Filer Information” form for a US-based accounting app.

import React, { useState } from 'react';

const TaxFilerForm = () => {
  // Initializing state for the filer's name and SSN (last 4 digits)
  const [fullName, setFullName] = useState('');
  const [zipCode, setZipCode] = useState('');

  const handleNameChange = (e) => {
    setFullName(e.target.value);
  };

  const handleZipChange = (e) => {
    // Only allow numbers and max 5 digits for US Zip Code
    const value = e.target.value;
    if (/^\d{0,5}$/.test(value)) {
      setZipCode(value);
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log("Submitting Tax Data for:", fullName, "in Zip:", zipCode);
    alert(`Form submitted for ${fullName}`);
  };

  return (
    <div style={{ padding: '20px', maxWidth: '400px' }}>
      <h2>Annual Tax Filing - Basic Info</h2>
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '10px' }}>
          <label>Full Name (as per SSN): </label>
          <input 
            type="text" 
            value={fullName} 
            onChange={handleNameChange} 
            placeholder="John Doe"
            style={{ display: 'block', width: '100%' }}
          />
        </div>
        <div style={{ marginBottom: '10px' }}>
          <label>US Zip Code: </label>
          <input 
            type="text" 
            value={zipCode} 
            onChange={handleZipChange} 
            placeholder="90210"
            style={{ display: 'block', width: '100%' }}
          />
        </div>
        <button type="submit">Register Filing</button>
      </form>
      <p>Current State Preview: {fullName} | {zipCode}</p>
    </div>
  );
};

export default TaxFilerForm;

You can see the output in the screenshot below.

React Controlled vs Uncontrolled Components

In this example, I used value={fullName}. This means the input box will always show exactly what is in our state.

The biggest advantage here is instant validation. Notice how I restricted the Zip Code field to only five digits in the handleZipChange function.

What are Uncontrolled Components in React?

Uncontrolled components act more like traditional HTML form elements. The form data is handled by the DOM itself, not by React state.

Instead of writing an event handler for every state update, you use a ref to pull the value from the field when you need it.

Method 2: Use the useRef Hook for Uncontrolled Inputs

I often use this method when I need to integrate with non-React libraries or when I want to build a very simple form where I don’t need real-time validation.

Let’s look at a “Customer Feedback” form for a local coffee shop in Seattle.

import React, { useRef } from 'react';

const CoffeeFeedbackForm = () => {
  // Creating references for the DOM elements
  const nameRef = useRef(null);
  const feedbackRef = useRef(null);
  const storeLocationRef = useRef(null);

  const handleSubmit = (event) => {
    event.preventDefault();
    
    // Accessing values directly from the DOM via refs
    const customerData = {
      name: nameRef.current.value,
      feedback: feedbackRef.current.value,
      location: storeLocationRef.current.value
    };

    console.log("Feedback Received:", customerData);
    alert(`Thank you ${customerData.name}! Your feedback for ${customerData.location} has been sent.`);
  };

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc' }}>
      <h3>Starbucks Seattle - Feedback Form</h3>
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '10px' }}>
          <label>Customer Name:</label>
          <input 
            type="text" 
            ref={nameRef} 
            defaultValue="Valued Guest" 
            style={{ display: 'block' }}
          />
        </div>
        <div style={{ marginBottom: '10px' }}>
          <label>Store Location:</label>
          <select ref={storeLocationRef} style={{ display: 'block' }}>
            <option value="Pike Place">Pike Place Market</option>
            <option value="Capitol Hill">Capitol Hill</option>
            <option value="University District">University District</option>
          </select>
        </div>
        <div style={{ marginBottom: '10px' }}>
          <label>Comments:</label>
          <textarea 
            ref={feedbackRef} 
            style={{ display: 'block', width: '100%' }}
          />
        </div>
        <button type="submit">Submit Feedback</button>
      </form>
    </div>
  );
};

export default CoffeeFeedbackForm;

You can see the output in the screenshot below.

Controlled vs Uncontrolled Components in React

In this case, I used defaultValue instead of value. This sets the initial value but lets the DOM take over from there.

This approach is much faster to write because you don’t need to create state variables or change handlers for every single input.

Key Differences Between Controlled and Uncontrolled

Through the years, I’ve found that the choice usually boils down to how much interaction you need with the data before the user hits “Submit.”

Feature Comparison Table

FeatureControlledUncontrolled
Source of TruthReact StateDOM
ValidationInstant / Per-keystrokeOn Submit
Code ComplexityHigherLower
PerformanceCan be slower for huge formsFaster (no re-renders on typing)
PredictabilityHighLow

When to Choose Controlled Components

I almost always go with controlled components for professional, enterprise-grade applications.

If you need to disable a “Submit” button until all fields are valid, you need controlled components.

If you need specific formatting (like adding dashes to a phone number as the user types), controlled components are the way to go.

Example: Real-time Credit Card Formatting

Here is a complex example of a controlled component used for a US Credit Card input.

import React, { useState } from 'react';

const PaymentForm = () => {
  const [cardNum, setCardNum] = useState('');

  const handleCardChange = (e) => {
    let value = e.target.value.replace(/\D/g, ''); // Remove non-digits
    if (value.length > 16) value = value.slice(0, 16);
    
    // Add spaces every 4 digits for American Express/Visa style
    const formattedValue = value.replace(/(\d{4})(?=\d)/g, '$1 ');
    setCardNum(formattedValue);
  };

  return (
    <div style={{ padding: '20px' }}>
      <label>Enter Credit Card Number:</label>
      <input 
        type="text" 
        value={cardNum} 
        onChange={handleCardChange} 
        placeholder="xxxx xxxx xxxx xxxx"
      />
      {cardNum.length > 0 && cardNum.length < 19 && (
        <p style={{ color: 'red' }}>Invalid card length</p>
      )}
    </div>
  );
};

When to Choose Uncontrolled Components

Don’t let people tell you uncontrolled components are “bad.” They have their place.

I use them often when I’m dealing with file inputs (<input type=”file” />). In React, file inputs are always uncontrolled because the data is read-only.

They are also great for internal tools where performance is more important than a fancy UI, and you have a massive amount of inputs.

Example: Handle File Uploads (Always Uncontrolled)

import React, { useRef } from 'react';

const ResumeUpload = () => {
  const fileInput = useRef(null);

  const handleUpload = (e) => {
    e.preventDefault();
    const fileName = fileInput.current.files[0].name;
    alert(`Selected resume: ${fileName}`);
  };

  return (
    <form onSubmit={handleUpload} style={{ padding: '20px' }}>
      <label>Upload your Resume (PDF):</label>
      <input type="file" ref={fileInput} />
      <br /><br />
      <button type="submit">Upload to Portal</button>
    </form>
  );
};

Integrate with Form Libraries

In my recent projects, I’ve stopped writing these patterns from scratch for large forms.

Instead, I use libraries like Formik or React Hook Form.

Interestingly, React Hook Form uses uncontrolled components under the hood to improve performance, but it provides an API that feels like controlled components.

It’s the best of both worlds, and I highly recommend looking into it if you’re building a production app for a US client.

Performance Considerations

One thing I’ve noticed in high-traffic apps is that controlled components can cause lag if the component is very complex.

Every time a user types a single letter, the entire component re-renders.

If you notice typing lag, you might want to move that specific input into its own smaller component or switch to an uncontrolled approach.

In this guide, I have covered the fundamental differences between controlled and uncontrolled components in React.

Choosing between them depends on your specific needs, but knowing how both work is essential for any professional React developer.

You may also like to read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.