8 min read

Controlled vs. Uncontrolled Inputs in React

Controlled vs. Uncontrolled Inputs in React

If you've followed along with my previous articles on How to Install React and Create Your First Function and Class Components and Props vs State in React — Real-World Use Cases, you're well equipped to dive into a topic that often trips up both new and experienced React developers: Controlled vs. Uncontrolled Inputs.

Don’t let the technical terms worry you. We’ll break both concepts down to their basics and work through clear, hands-on order form examples, ensuring that even if you’ve never touched React before, you can follow and experiment — and actually understand why these patterns matter in real projects.


A Quick Recap: How React Handles Data

Before jumping in, remember this cornerstone:

  • State: Data managed within a React component, used to keep track of what is shown on the screen, updated via user input or other means.
  • Props: Data passed to a component from its parent, but not owned or updated by that component.

This is crucial for understanding how input elements, like forms, interact with the rest of your React app.


What Are Controlled Inputs?

Controlled inputs are form elements (like <input><textarea>, or <select>) whose values are fully managed by React state. That means, whatever you type into a text field, the displayed value is always determined by a variable in your React component.

How does it work? You tie the input value directly to a state variable, and listen for changes using onChange. When the user types in the field, your code updates the React state, and the displayed value reflects that change.

Example: Order Creation Form (Controlled)

Let's build a basic order form where you specify the customer name and order amount.

Step 1: The Controlled Input Component

// OrderFormControlled.js
import React, { useState } from 'react';

function OrderFormControlled({ onCreateOrder }) {
  const [customer, setCustomer] = useState('');
  const [amount, setAmount] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (customer && amount) {
      onCreateOrder({ customer, amount: Number(amount) });
      setCustomer('');
      setAmount('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Customer Name:
        <input
          value={customer}
          onChange={e => setCustomer(e.target.value)}
          type="text"
        />
      </label>
      <br />
      <label>
        Order Amount:
        <input
          value={amount}
          min={0}
          onChange={e => setAmount(e.target.value)}
          type="number"
        />
      </label>
      <br />
      <button type="submit">Create Order</button>
    </form>
  );
}

export default OrderFormControlled;

How to Try This Locally:

  1. In your React project (created, for example, with npx create-react-app my-app), save the above file as OrderFormControlled.js.
  2. Now, update your App.js like this:
// App.js
import React, { useState } from 'react';
import OrderFormControlled from './OrderFormControlled';

function App() {
  const [orders, setOrders] = useState([]);

  const handleCreateOrder = (order) => {
    const newOrder = { id: orders.length + 1, ...order };
    setOrders([...orders, newOrder]);
  };

  return (
    <div>
      <h2>New Order (Controlled)</h2>
      <OrderFormControlled onCreateOrder={handleCreateOrder} />
      <ul>
        {orders.map(order => (
          <li key={order.id}>
            Order #{order.id}: {order.customer} — ${order.amount}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Run your app with npm start and try creating some orders!

What You'll Notice:

  • Whenever you type in the form, the text fields re-render with the values from React state.
  • Your UI is always in sync with your internal data.

Benefits:

  • All data is centralized and validated.
  • You can reset the form instantly by updating state.
  • Useful for dynamic validation, real-time input formatting, and modular UI updates.

What Are Uncontrolled Inputs?

Uncontrolled inputs are form elements that manage their own internal state. Instead of React tracking their current value, you let the browser handle it (just like with ordinary HTML forms). If you want their value, you use a ref to read it directly.

This approach is sometimes simpler for very basic forms and when you need to integrate with non-React code or libraries.

Example: Order Creation Form (Uncontrolled)

Here’s how you could build the same order form using uncontrolled inputs.

Step 1: The Uncontrolled Input Component

// OrderFormUncontrolled.js
import React, { useRef } from 'react';

function OrderFormUncontrolled({ onCreateOrder }) {
  const customerRef = useRef();
  const amountRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    const customer = customerRef.current.value;
    const amount = amountRef.current.value;
    if (customer && amount) {
      onCreateOrder({ customer, amount: Number(amount) });
      customerRef.current.value = '';
      amountRef.current.value = '';
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Customer Name:
        <input ref={customerRef} type="text" />
      </label>
      <br />
      <label>
        Order Amount:
        <input ref={amountRef} type="number" />
      </label>
      <br />
      <button type="submit">Create Order</button>
    </form>
  );
}

export default OrderFormUncontrolled;

To use this in your app:

  1. Save as OrderFormUncontrolled.js.
  2. In App.js, swap out the import and usage:
// App.js
import React, { useState } from 'react';
import OrderFormUncontrolled from './OrderFormUncontrolled';

function App() {
  const [orders, setOrders] = useState([]);

  const handleCreateOrder = (order) => {
    const newOrder = { id: orders.length + 1, ...order };
    setOrders([...orders, newOrder]);
  };

  return (
    <div>
      <h2>New Order (Uncontrolled)</h2>
      <OrderFormUncontrolled onCreateOrder={handleCreateOrder} />
      <ul>
        {orders.map(order => (
          <li key={order.id}>
            Order #{order.id}: {order.customer} — ${order.amount}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Try it again using npm start! Notice how you can still submit and reset, but React isn’t actively managing what you type into each box — it simply grabs the value when needed.

At first glance, the forms look similar because both collect the same data (customer name and order amount) and show results in a list. However, in React, controlled and uncontrolled inputs are very different under the hood. Here’s a crystal-clear explanation with direct comparisons and metaphors:

Controlled ComponentUncontrolled Component
Who manages value?React manages the input value using useState—React’s state is always the source of truthThe DOM (browser) manages the value—React does not track or know what’s typed in real-time
Input’s valueSet via value={...}—whatever is in state is what shows in the boxSet via the DOM’s own value until submit; initialized with defaultValue or blank
On user typingFires onChange → sets state → React updates input valueUser types, browser updates input box directly, React is not notified—only reads when needed
How do you get the value?It’s always in state (e.g., customer and amount variables from useState)Use a ref (e.g., customerRef.current.value) to fetch it from the DOM when needed
When does React know value?On every keystroke—truly instantOnly after you access ref.current.value (e.g., on submit)
Ideal for...Real-time validation, controlled formatting, always-in-sync UIQuick forms, integrating non-React JS libraries, legacy support

Side-by-side: How the code acts in memory

Controlled Example

// In-memory, always in sync:
const [customer, setCustomer] = useState('');
<input value={customer} onChange={e => setCustomer(e.target.value)} />
  • User types "A" → setCustomer("A") is called, UI shows "A" (because value={customer}).
  • Delete letter → instantly updates state and UI.
  • React can validate, modify, reset instantly.

Uncontrolled Example

// Browser manages the box, React just peeks in:
const customerRef = useRef();
<input ref={customerRef} type="text" />
// On submit:
const customerName = customerRef.current.value;
  • User types "A" → browser displays the letter, React doesn’t know.
  • Only when you hit "submit" does React call customerRef.current.value to "read" the box.
  • React cannot easily validate or modify as the user types.

Comparing the Two: When to Use Which?

FeatureControlledUncontrolled
Input Value ManagementBy React (state)By DOM/browser
Setup ComplexitySlightly moreSimpler (for trivial forms)
Validation, FormattingEasy, in real-timeHarder, manual
Works for Complex FormsYesNot recommended
Libraries/Integration ScenariosAll React casesNeeded for legacy/lib code
Form ResettingInstant and declarativeImperative/manual
Guideline: For almost all forms in a modern React app, prefer controlled inputs. Only use uncontrolled for legacy support, or ultra-simple unchanging forms.

Why Does This Matter in Order Management?

Let’s say you’re building an order dashboard (like in earlier articles), and you want user input for new orders to:

  • Show validation hints as the user types (e.g., invalid amounts or empty fields).
  • Reset cleanly after save or cancel.
  • Persist or sync input values with global app state.

Controlled inputs make this easy! You can display warnings, disable submit buttons, or even show totals in real-time — all powered by React’s state.


Practical Enhancement: Adding Real-time Validation

Let’s add on-the-fly validation to the controlled form above:

// OrderFormControlledWithValidation.js
import React, { useState } from 'react';

function OrderFormControlledWithValidation({ onCreateOrder }) {
  const [customer, setCustomer] = useState('');
  const [amount, setAmount] = useState('');
  const [error, setError] = useState('');

  const handleCustomerChange = (e) => setCustomer(e.target.value);
  const handleAmountChange = (e) => setAmount(e.target.value);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!customer) {
      setError('Customer is required');
    } else if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
      setError('Enter a valid order amount');
    } else {
      setError('');
      onCreateOrder({ customer, amount: Number(amount) });
      setCustomer('');
      setAmount('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Customer Name:
        <input value={customer} onChange={handleCustomerChange} type="text" />
      </label>
      <br />
      <label>
        Order Amount:
        <input value={amount} onChange={handleAmountChange} type="number" />
      </label>
      <br />
      {error && <div style={{ color: 'red' }}>{error}</div>}
      <button type="submit">Create Order</button>
    </form>
  );
}

export default OrderFormControlledWithValidation;

Try swapping this new component into App.js the same way as the previous examples.


Extra: A Hybrid Example (Mixing Both)

It's rare, but you can combine both types. For example, if you want most fields controlled, but one field left uncontrolled (say, for focusing or integrating with a third-party widget).

import React, { useRef, useState } from 'react';

function OrderFormHybrid({ onCreateOrder }) {
  const [customer, setCustomer] = useState(''); // controlled
  const amountRef = useRef(); // uncontrolled for demo

  const handleSubmit = (e) => {
    e.preventDefault();
    const amount = amountRef.current.value;
    if (customer && amount) {
      onCreateOrder({ customer, amount: Number(amount) });
      setCustomer('');
      amountRef.current.value = '';
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Customer Name (controlled):
        <input value={customer} onChange={e => setCustomer(e.target.value)} type="text" />
      </label>
      <br />
      <label>
        Order Amount (uncontrolled):
        <input ref={amountRef} type="number" />
      </label>
      <br />
      <button type="submit">Create Order</button>
    </form>
  );
}

export default OrderFormHybrid;

Summary Table: Controlled vs Uncontrolled Inputs

ControlledUncontrolled
Value SourceReact StateDOM (element holds value)
Default ValueProvided via state/propsProvided via defaultValue
How to ReadDirect state variableUse Ref (ref.current.value)
ValidationOn every input (easy)Manual, with ref
Use State HookYesNo (unless hybrid)

Wrapping Up

Choosing between controlled and uncontrolled inputs is a defining decision for form handling in React apps. For most scalable order management UIs — or any real-world application — controlled inputs are the clear winner. You get powerful validation, easier resetting, and your data always stays in sync with your interface.


Next Steps

If forms seem hard at first, keep practicing with these examples (just like we did in the articles on function and class components and props vs state).

In the next article, we’ll tackle Keys in Lists and Why They Matter for Dynamic UIs — trust me, this is the secret to building lightning-fast dynamic lists in React, and it’ll make your dashboards truly scalable.

Try editing and experimenting with all the code here — every snippet is designed to drop right into your React App.js, so you can see the difference live and learn by doing.