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.

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.

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
| Feature | Controlled | Uncontrolled |
| Source of Truth | React State | DOM |
| Validation | Instant / Per-keystroke | On Submit |
| Code Complexity | Higher | Lower |
| Performance | Can be slower for huge forms | Faster (no re-renders on typing) |
| Predictability | High | Low |
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:
- How to Fix React Warning for contentEditable with Children
- How to Manage State in React Using Modern Hooks
- React Class Component State
- How to Build Scheduling Calendar Component in React

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.