Skip to main content

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​

FeatureControlled ComponentUncontrolled Component
State StorageReact Component State (useState)DOM Node directly
Data FlowPush on every keystroke (onChange)Pull when needed via useRef
Instant ValidationYes (clean, reactive validation)Difficult
Dynamic DisablingYesDifficult
Default PatternStandard in 95% of React formsUsed 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 value to React state and update it via onChange.
  • 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.name with computed property syntax [name]: value.
  • Checkboxes use e.target.checked instead of e.target.value.
  • File inputs are always uncontrolled; access them using useRef.