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:
- In your React project (created, for example, with
npx create-react-app my-app), save the above file asOrderFormControlled.js. - Now, update your
App.jslike 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:
- Save as
OrderFormUncontrolled.js. - 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 Component | Uncontrolled Component | |
|---|---|---|
| Who manages value? | React manages the input value using useState—React’s state is always the source of truth | The DOM (browser) manages the value—React does not track or know what’s typed in real-time |
| Input’s value | Set via value={...}—whatever is in state is what shows in the box | Set via the DOM’s own value until submit; initialized with defaultValue or blank |
| On user typing | Fires onChange → sets state → React updates input value | User 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 instant | Only after you access ref.current.value (e.g., on submit) |
| Ideal for... | Real-time validation, controlled formatting, always-in-sync UI | Quick 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.valueto "read" the box. - React cannot easily validate or modify as the user types.
Comparing the Two: When to Use Which?
| Feature | Controlled | Uncontrolled |
|---|---|---|
| Input Value Management | By React (state) | By DOM/browser |
| Setup Complexity | Slightly more | Simpler (for trivial forms) |
| Validation, Formatting | Easy, in real-time | Harder, manual |
| Works for Complex Forms | Yes | Not recommended |
| Libraries/Integration Scenarios | All React cases | Needed for legacy/lib code |
| Form Resetting | Instant and declarative | Imperative/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
| Controlled | Uncontrolled | |
|---|---|---|
| Value Source | React State | DOM (element holds value) |
| Default Value | Provided via state/props | Provided via defaultValue |
| How to Read | Direct state variable | Use Ref (ref.current.value) |
| Validation | On every input (easy) | Manual, with ref |
| Use State Hook | Yes | No (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.
Member discussion