Controlled Form Components: Part 2 Guide
Controlled form components manage all input values through React state. This guide teaches you to build real-world forms handling multiple input types (text, textarea, select, checkbox), implement validation, and provide user feedback. You'll create a reusable handleChange function, validate on submission, and disable submit buttons until the form is valid—essential patterns for production React applications.
Key Takeaways
- A single generic
handleChangefunction can handle all input types by checkingevent.target.typeand using[name]computed property syntax - Checkboxes use
checkedproperty; all other inputs usevalueproperty - Store validation errors in state and render them conditionally next to the relevant fields
- Call
validate()on form submission; only submit if no errors exist - Disable the submit button with
disabled={!isFormValid()}to improve user experience - Use
event.preventDefault()inhandleSubmitto prevent browser page reload
Prerequisites
Before starting, ensure you understand:
- Controlled components fundamentals (Part 1)
- JavaScript ES6 features: arrow functions, destructuring, computed property names
- React state management with
useState - Form event handling (
onChange,onSubmit)
Building a User Profile Form
Let's build a practical form that collects user information with multiple input types and validation. This form will include:
- Username (text input) — required, non-empty
- Bio (textarea) — required, minimum 10 characters
- Role (select dropdown) — Developer, Designer, or Admin
- Newsletter (checkbox) — optional subscription
This real-world example covers all common form patterns.
Step 1: Initialize State for Form Data and Errors
Start by creating state objects to store form data and validation errors:
import React, { useState } from 'react';
function UserProfileForm() {
const [profile, setProfile] = useState({
username: '',
bio: '',
role: 'Developer', // Default value for select
newsletter: true, // Default for checkbox
});
const [errors, setErrors] = useState({});
const handleChange = (event) => {
// Implementation next
};
const handleSubmit = (event) => {
// Implementation next
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields here */}
</form>
);
}
export default UserProfileForm;
Explanation:
profileobject holds all form field valueserrorsobject holds validation error messages keyed by field name (e.g.,errors.username)- Default values for
roleandnewsletterimprove UX
Step 2: Create the Generic handleChange Function
A single handleChange function can manage all input types by detecting the input type and using the correct value source:
const handleChange = (event) => {
const { name, value, type, checked } = event.target;
// Checkboxes use 'checked'; all others use 'value'
const newValue = type === 'checkbox' ? checked : value;
setProfile(prevProfile => ({
...prevProfile,
[name]: newValue, // Computed property name
}));
};
Breakdown:
- Destructure
name,value,type, andcheckedfrom the event target - The ternary operator selects the correct property:
checkedfor checkboxes,valuefor everything else - The spread operator
...prevProfilecopies all existing properties [name]is a computed property name—uses the input'snameattribute as the object key- This pattern works for text inputs, textareas, selects, and checkboxes without modification
Step 3: Render All Form Input Types
Each input type has slightly different JSX, but they all use the same handleChange handler:
return (
<form onSubmit={handleSubmit}>
{/* Text Input */}
<div>
<label>
Username:
<input
type="text"
name="username"
value={profile.username}
onChange={handleChange}
required
/>
</label>
{errors.username && <p style={{ color: 'red' }}>{errors.username}</p>}
</div>
{/* Textarea */}
<div>
<label>
Bio:
<textarea
name="bio"
value={profile.bio}
onChange={handleChange}
rows={4}
/>
</label>
{errors.bio && <p style={{ color: 'red' }}>{errors.bio}</p>}
</div>
{/* Select Dropdown */}
<div>
<label>
Role:
<select name="role" value={profile.role} onChange={handleChange}>
<option value="Developer">Developer</option>
<option value="Designer">Designer</option>
<option value="Admin">Admin</option>
</select>
</label>
</div>
{/* Checkbox */}
<div>
<label>
<input
type="checkbox"
name="newsletter"
checked={profile.newsletter}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
</div>
<button type="submit" disabled={!isFormValid()}>
Save Profile
</button>
</form>
);
Key Points:
- Each input's
nameattribute must match a property in theprofilestate object - Text inputs and textareas use
value={profile.fieldName} - Select dropdowns use
value={profile.fieldName}and wrap options - Checkboxes use
checked={profile.fieldName}instead ofvalue - All use the same
onChange={handleChange}handler - Error messages render conditionally:
{errors.fieldName && <p>...}</p>
Step 4: Implement Validation Logic
Create a validate() function that checks form data and returns an object with any errors:
const validate = () => {
const newErrors = {};
if (!profile.username.trim()) {
newErrors.username = 'Username is required';
}
if (profile.bio.length < 10) {
newErrors.bio = 'Bio must be at least 10 characters';
}
// Add more validations as needed
return newErrors;
};
Validation Rules Applied:
- Username is required and cannot be empty (using
trim()to ignore whitespace) - Bio must be at least 10 characters long
- Add more rules by extending this function
Step 5: Handle Form Submission
Implement handleSubmit to prevent default browser behavior, validate, and either show errors or process the form:
const handleSubmit = (event) => {
event.preventDefault(); // Prevent page reload
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
// Form has errors
setErrors(newErrors);
} else {
// Form is valid
setErrors({});
console.log('Profile submitted:', profile);
alert('Profile saved successfully!');
// In a real app, send data to a server here
}
};
const isFormValid = () => {
const newErrors = validate();
return Object.keys(newErrors).length === 0;
};
Breakdown:
event.preventDefault()stops the browser from submitting the form to a servervalidate()returns an errors objectObject.keys(newErrors).lengthchecks if there are any errors- If errors exist, update the errors state so they display in the UI
- If no errors, clear previous errors and submit the form (log to console in this example)
Improving User Experience: Disable Invalid Submit
Disable the submit button while the form is invalid to guide users:
<button type="submit" disabled={!isFormValid()}>
Save Profile
</button>
The button is disabled until isFormValid() returns true. This happens after the user fills required fields and meets all validation requirements. This pattern is common in production apps.
Best Practices for React Forms
Do:
- Store all form state in a single object or separate pieces of state for clarity
- Use consistent
nameattributes that match state keys - Validate on submission (or optionally on blur after first attempt)
- Provide clear, field-specific error messages
- Use
event.preventDefault()inhandleSubmit - Disable submit buttons for invalid forms when appropriate
- Use textareas for longer text, not regular inputs
- Provide default values for select dropdowns
Don't:
- Don't validate on every keystroke unless absolutely necessary (performance impact)
- Don't clear all errors on the first keystroke after error display
- Don't mix controlled and uncontrolled inputs in one form
- Don't forget the
nameattribute on form inputs
Frequently Asked Questions
How do I validate only when the user leaves a field (on blur)?
Add an onBlur handler that validates just that field:
const handleBlur = (event) => {
const { name } = event.target;
const newErrors = { ...errors };
if (name === 'username' && !profile.username.trim()) {
newErrors.username = 'Username is required';
}
setErrors(newErrors);
};
<input onBlur={handleBlur} ... />
Can I disable specific buttons without disabling submit?
Yes, use conditional rendering or separate state. Only the submit button should be disabled based on form validity; other buttons (Cancel, Reset) stay enabled.
How do I handle form fields that aren't strings (numbers, dates)?
Store them as strings in state and convert on submission:
const age = parseInt(profile.age, 10);
const date = new Date(profile.date);
Should I validate in handleChange or handleSubmit?
Validate on handleSubmit (submission validation) for best UX. Only validate on handleChange if showing real-time feedback is critical (rare). The combination—validate on submit, then on blur after first attempt—is ideal.
How do I reset the form to initial state?
const handleReset = () => {
setProfile({ username: '', bio: '', role: 'Developer', newsletter: true });
setErrors({});
};
<button type="button" onClick={handleReset}>Reset</button>
Conclusion
Controlled form components are the React standard for handling user input. Master these patterns:
- Use a single
handleChangefunction with computed property names[name] - Store both form data and errors in state
- Validate on submission before processing data
- Provide immediate, field-specific error feedback
- Disable submit buttons for invalid forms to guide users
These patterns form the foundation of professional React form handling. Practice building increasingly complex forms to internalize them.