Controlled vs Uncontrolled Components
Understanding the difference between controlled and uncontrolled components is crucial for React development, as it affects how you manage form data and component state.
Table of Contents
- Controlled vs Uncontrolled Components
What are Controlled Components?
Controlled components are React components where the form data is handled by the component’s state and controlled by React.
Key Concepts:
- State-driven: Form data is stored in React state
- Single source of truth: React state controls the component
- Predictable: Component behavior is fully controlled by React
- Validation: Easy to implement real-time validation
Controlled Component Structure:
import React, { useState } from 'react';
function ControlledForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prevData => ({
...prevData,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
/>
<button type="submit">Submit</button>
</form>
);
}
What are Uncontrolled Components?
Uncontrolled components are React components where the form data is handled by the DOM itself, using refs to access the values.
Key Concepts:
- DOM-driven: Form data is managed by the DOM
- Refs: Use refs to access form values
- Less code: Simpler implementation for basic forms
- Performance: Fewer re-renders
Uncontrolled Component Structure:
import React, { useRef } from 'react';
function UncontrolledForm() {
const nameRef = useRef();
const emailRef = useRef();
const messageRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
const formData = {
name: nameRef.current.value,
email: emailRef.current.value,
message: messageRef.current.value
};
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<input
ref={nameRef}
type="text"
placeholder="Name"
/>
<input
ref={emailRef}
type="email"
placeholder="Email"
/>
<textarea
ref={messageRef}
placeholder="Message"
/>
<button type="submit">Submit</button>
</form>
);
}
Key Differences
1. State Management:
// Controlled: State manages form data
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Controlled input"
/>
);
}
// Uncontrolled: DOM manages form data
function UncontrolledInput() {
const inputRef = useRef();
const handleSubmit = () => {
console.log('Value:', inputRef.current.value);
};
return (
<div>
<input
ref={inputRef}
placeholder="Uncontrolled input"
/>
<button onClick={handleSubmit}>Get Value</button>
</div>
);
}
2. Validation:
// Controlled: Real-time validation
function ControlledInputWithValidation() {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
// Real-time validation
if (newValue.length < 3) {
setError('Value must be at least 3 characters');
} else {
setError('');
}
};
return (
<div>
<input
value={value}
onChange={handleChange}
placeholder="Enter value"
/>
{error && <span style={{ color: 'red' }}>{error}</span>}
</div>
);
}
// Uncontrolled: Validation on submit
function UncontrolledInputWithValidation() {
const inputRef = useRef();
const [error, setError] = useState('');
const handleSubmit = () => {
const value = inputRef.current.value;
if (value.length < 3) {
setError('Value must be at least 3 characters');
} else {
setError('');
console.log('Valid value:', value);
}
};
return (
<div>
<input
ref={inputRef}
placeholder="Enter value"
/>
<button onClick={handleSubmit}>Validate</button>
{error && <span style={{ color: 'red' }}>{error}</span>}
</div>
);
}
3. Performance:
// Controlled: Re-renders on every keystroke
function ControlledPerformance() {
const [value, setValue] = useState('');
console.log('Controlled component re-rendered');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Type here..."
/>
);
}
// Uncontrolled: No re-renders during typing
function UncontrolledPerformance() {
const inputRef = useRef();
console.log('Uncontrolled component re-rendered');
return (
<input
ref={inputRef}
placeholder="Type here..."
/>
);
}
Implementation Examples
1. Controlled Form with Validation:
function ControlledFormWithValidation() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validateField = (name, value) => {
switch (name) {
case 'name':
return value.length < 2 ? 'Name must be at least 2 characters' : '';
case 'email':
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return !emailRegex.test(value) ? 'Invalid email format' : '';
case 'password':
return value.length < 6 ? 'Password must be at least 6 characters' : '';
case 'confirmPassword':
return value !== formData.password ? 'Passwords do not match' : '';
default:
return '';
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// Validate field if it has been touched
if (touched[name]) {
const error = validateField(name, value);
setErrors(prev => ({
...prev,
[name]: error
}));
}
};
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched(prev => ({
...prev,
[name]: true
}));
const error = validateField(name, value);
setErrors(prev => ({
...prev,
[name]: error
}));
};
const handleSubmit = (e) => {
e.preventDefault();
// Validate all fields
const newErrors = {};
Object.keys(formData).forEach(key => {
const error = validateField(key, formData[key]);
if (error) {
newErrors[key] = error;
}
});
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
console.log('Form is valid:', formData);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Name"
/>
{touched.name && errors.name && (
<span style={{ color: 'red' }}>{errors.name}</span>
)}
</div>
<div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Email"
/>
{touched.email && errors.email && (
<span style={{ color: 'red' }}>{errors.email}</span>
)}
</div>
<div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Password"
/>
{touched.password && errors.password && (
<span style={{ color: 'red' }}>{errors.password}</span>
)}
</div>
<div>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Confirm Password"
/>
{touched.confirmPassword && errors.confirmPassword && (
<span style={{ color: 'red' }}>{errors.confirmPassword}</span>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}
2. Uncontrolled Form with File Upload:
function UncontrolledFileUpload() {
const fileInputRef = useRef();
const [selectedFiles, setSelectedFiles] = useState([]);
const handleFileSelect = () => {
const files = Array.from(fileInputRef.current.files);
setSelectedFiles(files);
};
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData();
selectedFiles.forEach(file => {
formData.append('files', file);
});
// Submit files to server
fetch('/api/upload', {
method: 'POST',
body: formData
});
};
return (
<form onSubmit={handleSubmit}>
<input
ref={fileInputRef}
type="file"
multiple
onChange={handleFileSelect}
accept="image/*,.pdf,.doc,.docx"
/>
{selectedFiles.length > 0 && (
<div>
<h4>Selected Files:</h4>
<ul>
{selectedFiles.map((file, index) => (
<li key={index}>
{file.name} ({(file.size / 1024).toFixed(2)} KB)
</li>
))}
</ul>
</div>
)}
<button type="submit" disabled={selectedFiles.length === 0}>
Upload Files
</button>
</form>
);
}
3. Mixed Approach:
function MixedForm() {
// Controlled state for form fields
const [formData, setFormData] = useState({
name: '',
email: ''
});
// Uncontrolled refs for file inputs
const fileInputRef = useRef();
const hiddenInputRef = useRef();
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
// Get controlled form data
const controlledData = { ...formData };
// Get uncontrolled file data
const files = fileInputRef.current.files;
const hiddenValue = hiddenInputRef.current.value;
const submitData = {
...controlledData,
files: Array.from(files),
hiddenValue
};
console.log('Submitting:', submitData);
};
return (
<form onSubmit={handleSubmit}>
{/* Controlled inputs */}
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{/* Uncontrolled inputs */}
<input
ref={fileInputRef}
type="file"
multiple
/>
<input
ref={hiddenInputRef}
type="hidden"
defaultValue="hidden-value"
/>
<button type="submit">Submit</button>
</form>
);
}
Advanced Patterns
1. Controlled Component with Custom Hook:
function useForm(initialValues = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prev => ({
...prev,
[name]: value
}));
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched(prev => ({
...prev,
[name]: true
}));
};
const setFieldValue = (name, value) => {
setValues(prev => ({
...prev,
[name]: value
}));
};
const setFieldError = (name, error) => {
setErrors(prev => ({
...prev,
[name]: error
}));
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
setFieldValue,
setFieldError,
reset
};
}
// Usage
function FormWithCustomHook() {
const {
values,
errors,
touched,
handleChange,
handleBlur,
reset
} = useForm({
name: '',
email: ''
});
return (
<form>
<input
name="name"
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Name"
/>
{touched.name && errors.name && (
<span style={{ color: 'red' }}>{errors.name}</span>
)}
<input
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="Email"
/>
{touched.email && errors.email && (
<span style={{ color: 'red' }}>{errors.email}</span>
)}
<button type="button" onClick={reset}>Reset</button>
</form>
);
}
2. Uncontrolled Component with Imperative Handle:
const UncontrolledInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
blur: () => inputRef.current.blur(),
getValue: () => inputRef.current.value,
setValue: (value) => {
inputRef.current.value = value;
},
select: () => inputRef.current.select()
}));
return <input ref={inputRef} {...props} />;
});
// Usage
function ParentComponent() {
const inputRef = useRef();
const handleFocus = () => {
inputRef.current.focus();
};
const handleGetValue = () => {
console.log('Input value:', inputRef.current.getValue());
};
const handleSetValue = () => {
inputRef.current.setValue('New value');
};
return (
<div>
<UncontrolledInput ref={inputRef} placeholder="Type here..." />
<button onClick={handleFocus}>Focus</button>
<button onClick={handleGetValue}>Get Value</button>
<button onClick={handleSetValue}>Set Value</button>
</div>
);
}
3. Dynamic Form Fields:
function DynamicForm() {
const [fields, setFields] = useState([
{ id: 1, name: 'field1', value: '' }
]);
const addField = () => {
const newField = {
id: Date.now(),
name: `field${fields.length + 1}`,
value: ''
};
setFields(prev => [...prev, newField]);
};
const removeField = (id) => {
setFields(prev => prev.filter(field => field.id !== id));
};
const updateField = (id, value) => {
setFields(prev => prev.map(field =>
field.id === id ? { ...field, value } : field
));
};
const handleSubmit = (e) => {
e.preventDefault();
const formData = fields.reduce((acc, field) => {
acc[field.name] = field.value;
return acc;
}, {});
console.log('Form data:', formData);
};
return (
<form onSubmit={handleSubmit}>
{fields.map(field => (
<div key={field.id}>
<input
value={field.value}
onChange={(e) => updateField(field.id, e.target.value)}
placeholder={`Field ${field.name}`}
/>
<button type="button" onClick={() => removeField(field.id)}>
Remove
</button>
</div>
))}
<button type="button" onClick={addField}>
Add Field
</button>
<button type="submit">Submit</button>
</form>
);
}
Best Practices
1. Choose Based on Use Case:
// Use controlled for complex forms with validation
function ComplexForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Real-time validation
validateField(name, value);
};
return (
<form>
<input
name="name"
value={formData.name}
onChange={handleChange}
/>
{errors.name && <span>{errors.name}</span>}
</form>
);
}
// Use uncontrolled for simple file uploads
function SimpleFileUpload() {
const fileRef = useRef();
const handleSubmit = () => {
const files = fileRef.current.files;
// Process files
};
return (
<form onSubmit={handleSubmit}>
<input ref={fileRef} type="file" />
<button type="submit">Upload</button>
</form>
);
}
2. Performance Considerations:
// Good: Use uncontrolled for performance-critical inputs
function PerformanceOptimizedForm() {
const [criticalData, setCriticalData] = useState('');
const nonCriticalRef = useRef();
return (
<form>
{/* Controlled for critical data */}
<input
value={criticalData}
onChange={(e) => setCriticalData(e.target.value)}
placeholder="Critical data"
/>
{/* Uncontrolled for non-critical data */}
<input
ref={nonCriticalRef}
placeholder="Non-critical data"
/>
</form>
);
}
3. Validation Strategy:
// Good: Controlled for real-time validation
function ValidatedForm() {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
// Real-time validation
if (newValue.length < 3) {
setError('Too short');
} else {
setError('');
}
};
return (
<div>
<input value={value} onChange={handleChange} />
{error && <span style={{ color: 'red' }}>{error}</span>}
</div>
);
}
Common Interview Questions
Q: What is the difference between controlled and uncontrolled components?
- Controlled components manage form data through React state, while uncontrolled components use DOM refs to access form values.
Q: When should you use controlled components?
- For complex forms with validation, real-time updates, or when you need to control the form state programmatically.
Q: When should you use uncontrolled components?
- For simple forms, file uploads, or when you want better performance with minimal re-renders.
Q: What are the advantages of controlled components?
- Predictable behavior, easy validation, and full control over form state.
Q: What are the advantages of uncontrolled components?
- Better performance, less code, and easier integration with non-React code.
Q: How do you handle form validation in controlled components?
- Use state to track errors and validate on change or blur events.
Q: How do you handle form validation in uncontrolled components?
- Validate on form submission or use refs to access values when needed.
Q: Can you mix controlled and uncontrolled components?
- Yes, you can use controlled components for some fields and uncontrolled for others in the same form.
Q: What’s the performance impact of controlled vs uncontrolled?
- Controlled components re-render on every change, while uncontrolled components don’t re-render during typing.
Q: How do you convert between controlled and uncontrolled components?
- Add/remove value prop and onChange handler for controlled, or add/remove ref for uncontrolled.
Summary
- Controlled components use React state to manage form data
- Uncontrolled components use DOM refs to access form values
- Choose based on use case: controlled for complex forms, uncontrolled for simple inputs
- Performance considerations: uncontrolled components have fewer re-renders
- Validation strategies: controlled allows real-time validation, uncontrolled validates on submit
- Best practices include choosing the right approach and considering performance needs
- Understanding both patterns is essential for effective React form development
Interview angle
- “Controlled or uncontrolled?” - controlled when React state is the source of truth (
valueplusonChange): needed for validation as you type, conditional formatting, or driving other UI from the field. Uncontrolled when the DOM owns the value and you read it on submit via a ref orFormData- simpler and faster for large forms. - “What causes the ‘changing an uncontrolled input to be controlled’ warning?” - the
valuestarted asundefinedornulland later became a string. Initialise to"", notundefined. - “Why do large controlled forms get slow?” - every keystroke re-renders the subtree. Fixes: keep field state local to the field, use uncontrolled inputs with a form library, or debounce the derived work rather than the input itself.
- “What changed in React 19?” - form Actions plus
useActionStateanduseFormStatushandle submission, pending state and errors without a controlled-state scaffold, which removes much of the reason to control every field.