Proper Event Handling in Controlled Components
Controlled components in React require careful event handling to properly manage form inputs and user interactions. Here’s how to handle events correctly in controlled components.
The Core Principle
In controlled components:
- The component controls the input value via state
- Changes are handled through React event handlers
- The input’s value never changes without going through React’s state management
Common Mistakes
1. Not Updating State on Changes
// ❌ Wrong - input becomes read-only
function Input() {
const [value, setValue] = useState('');
return <input value={value} />; // Missing onChange
}
2. Using Uncontrolled Pattern Accidentally
// ❌ Wrong - mixing controlled and uncontrolled
function Input() {
const [value, setValue] = useState('');
return <input defaultValue={value} />; // Should be value=
}
3. Incorrect Event Handling
// ❌ Wrong - not using the event properly
function Input() {
const [value, setValue] = useState('');
const handleChange = () => {
setValue('fixed value'); // Doesn't use event
};
return <input value={value} onChange={handleChange} />;
}
Correct Patterns
1. Basic Controlled Input
// ✅ Correct - full control
function Input() {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value); // Update state with new value
};
return <input value={value} onChange={handleChange} />;
}
2. Handling Different Input Types
function Form() {
const [form, setForm] = useState({
name: '',
email: '',
subscribe: false,
size: 'medium'
});
// ✅ Universal handler for different inputs
const handleChange = (event) => {
const { name, value, type, checked } = event.target;
setForm(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
return (
<form>
<input
name="name"
value={form.name}
onChange={handleChange}
/>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
/>
<input
name="subscribe"
type="checkbox"
checked={form.subscribe}
onChange={handleChange}
/>
<select
name="size"
value={form.size}
onChange={handleChange}
>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</form>
);
}
3. Form Submission
function ContactForm() {
const [form, setForm] = useState({ name: '', email: '' });
const handleChange = (event) => {
const { name, value } = event.target;
setForm(prev => ({ ...prev, [name]: value }));
};
// ✅ Proper form submission
const handleSubmit = (event) => {
event.preventDefault();
// Submit form data
console.log(form);
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={form.name}
onChange={handleChange}
/>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
/>
<button type="submit">Submit</button>
</form>
);
}
Best Practices
- Always pair
value
withonChange
in controlled components - Use proper event types:
onChange
for inputs, selects, textareasonClick
for buttonsonSubmit
for forms
- Name your inputs consistently with your state
- Use computed property names for dynamic state updates
- Consider form libraries like Formik for complex forms
Performance Optimization
// ✅ Memoized handler for performance
function Input() {
const [value, setValue] = useState('');
const handleChange = useCallback((event) => {
setValue(event.target.value);
}, []);
return <input value={value} onChange={handleChange} />;
}
Common Pitfalls
- Async state updates: Remember state updates are async
const handleChange = (event) => {
setValue(event.target.value);
console.log(value); // ❌ Will log old value
};
- Missing initial state: Always initialize controlled values
const [value, setValue] = useState(''); // ✅ Initialize
- Overwriting state: Remember to spread previous state
// ❌ Wrong - loses other form fields
setForm({ [name]: value });
// ✅ Correct - preserves other fields
setForm(prev => ({ ...prev, [name]: value }));
By following these patterns, you’ll ensure your controlled components work predictably and efficiently while providing the best user experience.