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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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
Default to controlled components for most form scenarios—they make your UI more predictable and easier to debug.
-
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
-
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)
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)