2.7 Forms & Controlled Components
← Back to Chapter 2: React Fundamentals
In standard HTML, form elements like <input>, <textarea>, and <select> maintain their own internal state in the DOM. In React, mutable state is kept in component state and updated via setState. An input whose value is controlled by React is called a Controlled Component.
1. Controlled vs Uncontrolled Components
| Feature | Controlled Component | Uncontrolled Component |
|---|---|---|
| State Storage | React Component State (useState) | DOM Node directly |
| Data Flow | Push on every keystroke (onChange) | Pull when needed via useRef |
| Instant Validation | Yes (clean, reactive validation) | Difficult |
| Dynamic Disabling | Yes | Difficult |
| Default Pattern | Standard in 95% of React forms | Used for file inputs or quick prototypes |
2. The Controlled Input Pattern
In a controlled component, the React state serves as the Single Source of Truth:
import { useState } from 'react';
function SimpleSearch() {
const [query, setQuery] = useState("");
return (
<div>
<input
type="text"
value={query} // React dictates the displayed text
onChange={(e) => setQuery(e.target.value)} // React updates state
placeholder="Type to search..."
/>
<p>Current value: {query}</p>
<button onClick={() => setQuery("")}>Clear</button>
</div>
);
}
3. Handling Multiple Inputs with a Single State Object
Instead of creating 10 individual useState hooks for a 10-field form, group the form data into a single object state and leverage the HTML name attribute:
import { useState } from 'react';
function RegistrationForm() {
const [formData, setFormData] = useState({
username: "",
email: "",
role: "developer",
agreeTerms: false
});
function handleChange(e) {
const { name, value, type, checked } = e.target;
// For checkboxes read 'checked', otherwise read 'value'
const finalValue = type === "checkbox" ? checked : value;
setFormData(prev => ({
...prev,
[name]: finalValue // Dynamic computed property key
}));
}
function handleSubmit(e) {
e.preventDefault();
console.log("Submitting payload:", formData);
}
return (
<form onSubmit={handleSubmit} className="form-card">
<label>
Username:
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
/>
</label>
<label>
Email:
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</label>
<label>
Role:
<select name="role" value={formData.role} onChange={handleChange}>
<option value="developer">Developer</option>
<option value="designer">Designer</option>
<option value="manager">Manager</option>
</select>
</label>
<label>
<input
type="checkbox"
name="agreeTerms"
checked={formData.agreeTerms}
onChange={handleChange}
/>
I agree to the terms
</label>
<button type="submit" disabled={!formData.agreeTerms}>
Register Account
</button>
</form>
);
}
4. Uncontrolled Inputs with useRef
Sometimes you don't need to control every keystroke (e.g. <input type="file" />, which is always read-only in browsers and cannot be controlled programmatically):
import { useRef } from 'react';
function FileUploadForm() {
const fileInputRef = useRef(null);
function handleSubmit(e) {
e.preventDefault();
const selectedFile = fileInputRef.current.files[0];
console.log("Selected file:", selectedFile?.name);
}
return (
<form onSubmit={handleSubmit}>
<input type="file" ref={fileInputRef} />
<button type="submit">Upload</button>
</form>
);
}
5. Summary Checklist
- Controlled components bind the input's
valueto React state and update it viaonChange. - React state acts as the Single Source of Truth for form inputs.
- Group complex form inputs into a single state object and use
e.target.namewith computed property syntax[name]: value. - Checkboxes use
e.target.checkedinstead ofe.target.value. - File inputs are always uncontrolled; access them using
useRef.