Controlled Components
Controlled components in React: state as source of truth for inputs, checkboxes, selects, multi-field forms, validation, resets, and controlled vs uncontrolled.
Controlled Components
Controlled Components are one of the foundational pieces of modern React engineering.
Forms are a major part of almost every React application. Users enter names, emails, passwords, search queries, addresses, preferences, and other information. React needs a reliable way to know what the user has entered.
A controlled component is a form element whose current value is controlled by React state.
React State↓Form Element↑onChange↑User Input
React becomes the source of truth for the form value.
Learning Objectives
- What controlled components are and how the state ↔ input cycle works
- Controlled vs uncontrolled form elements
- Text, textarea, select, checkbox, and radio patterns
- Multi-field form objects, spreads, and functional updates
- Validation, derived UI, resets, limits, and transforms
- Common mistakes, best practices, and interview questions
1. What Is a Controlled Component?
A normal input lets the browser manage the value:
<input type="text" />
In a controlled React input, the value comes from state:
import { useState } from "react";function NameForm() {const [name, setName] = useState("");return (<inputvalue={name}onChange={(event) => {setName(event.target.value);}}/>);}
User types↓onChange↓setName()↓name state changes↓React renders↓value={name}↓Input displays updated value
2. React as the Source of Truth
In a controlled component, React state is the source of truth for the form value.
React State│▼value={state}│▼<input>│User types│▼onChange│▼setState()│└──────────────► React State
3. Basic Controlled Input
import { useState } from "react";function App() {const [name, setName] = useState("");return (<div><inputvalue={name}onChange={(event) => {setName(event.target.value);}}/><p>Your name: {name}</p></div>);}
If the user types Rahul, the UI displays Your name: Rahul. React knows the current value at all times.
4. Why Controlled Components Are Useful
They make it easy to:
- Validate user input
- Display live values and character counts
- Disable/enable buttons conditionally
- Reset forms and transform input
- Show validation errors and submit data
- Control dependent fields and synchronize inputs
<p>Characters: {name.length}</p>
5. Controlled vs Uncontrolled
Controlled
<inputvalue={name}onChange={handleChange}/>
Uncontrolled
<input ref={inputRef} />
Controlled: React State → Input ← onChangeUncontrolled: DOM → Input value → ref → React reads when needed
6. Controlled Text Input
function Profile() {const [name, setName] = useState("");function handleChange(event) {setName(event.target.value);}return (<inputtype="text"value={name}onChange={handleChange}/>);}
Both value and onChange are generally needed for a fully controlled input.
7. The onChange Handler
function handleChange(event) {console.log(event.target.value);setName(event.target.value);}
As the user types React, values progress: R → Re → Rea → Reac → React.
8. Controlled Inputs with Initial Values
const [name, setName] = useState("Rahul");<inputvalue={name}onChange={handleChange}/>
Existing profile → Load data → Initialize state → Controlled form → User edits → State updates
9. Controlled Inputs and Validation
function EmailForm() {const [email, setEmail] = useState("");const [error, setError] = useState("");function handleChange(event) {const value = event.target.value;setEmail(value);if (!value.includes("@")) {setError("Enter a valid email");} else {setError("");}}return (<div><input type="email" value={email} onChange={handleChange} />{error && <p>{error}</p>}</div>);}
10. Controlled Components and Derived UI
function SearchForm() {const [query, setQuery] = useState("");return (<div><inputvalue={query}onChange={(event) => setQuery(event.target.value)}/><p>Searching for: {query}</p><button disabled={query.trim() === ""}>Search</button></div>);}
11. Controlled Textarea
function MessageForm() {const [message, setMessage] = useState("");return (<><textareavalue={message}onChange={(event) => setMessage(event.target.value)}/><p>{message.length} characters</p></>);}
12. Controlled Select
function CountrySelect() {const [country, setCountry] = useState("");return (<selectvalue={country}onChange={(event) => setCountry(event.target.value)}><option value="">Select a country</option><option value="india">India</option><option value="usa">USA</option><option value="uk">United Kingdom</option></select>);}
13. Controlled Checkboxes
Checkboxes use checked instead of value:
function Notifications() {const [enabled, setEnabled] = useState(false);return (<label><inputtype="checkbox"checked={enabled}onChange={(event) => setEnabled(event.target.checked)}/>Enable notifications</label>);}
Text input → valueCheckbox → checked
14. Controlled Radio Buttons
function GenderForm() {const [gender, setGender] = useState("");return (<div><label><inputtype="radio"name="gender"value="male"checked={gender === "male"}onChange={(event) => setGender(event.target.value)}/>Male</label><label><inputtype="radio"name="gender"value="female"checked={gender === "female"}onChange={(event) => setGender(event.target.value)}/>Female</label></div>);}
15. Multiple Controlled Inputs
const [form, setForm] = useState({name: "",email: "",phone: ""});<input name="name" value={form.name} onChange={handleChange} /><input name="email" value={form.email} onChange={handleChange} /><input name="phone" value={form.phone} onChange={handleChange} />
16. One Handler for Multiple Inputs
function handleChange(event) {const { name, value } = event.target;setForm((currentForm) => ({...currentForm,[name]: value}));}
The name attribute determines which property should change.
17. Why the Spread Operator Is Important
Do not replace the entire object with only the changed field — other properties would be lost. Spread preserves them:
setForm((currentForm) => ({...currentForm,email: "rahul@example.com"}));
Existing form (name, email, phone)↓...currentForm + changed field↓New form object
18. Functional State Updates
setForm((currentForm) => ({...currentForm,[name]: value}));
Especially useful when multiple state updates may occur close together.
19. Controlled Components and Form Submission
function LoginForm() {const [email, setEmail] = useState("");const [password, setPassword] = useState("");function handleSubmit(event) {event.preventDefault();console.log({ email, password });}return (<form onSubmit={handleSubmit}><inputtype="email"value={email}onChange={(event) => setEmail(event.target.value)}/><inputtype="password"value={password}onChange={(event) => setPassword(event.target.value)}/><button type="submit">Login</button></form>);}
20. Controlled Components and Resetting
const initialForm = {name: "",email: "",password: ""};const [form, setForm] = useState(initialForm);function handleReset() {setForm(initialForm);}
setForm(initialForm) → State changes → React renders → Input values reset
21. Controlled Components and Conditional UI
function AccountForm() {const [accountType, setAccountType] = useState("");return (<div><selectvalue={accountType}onChange={(event) => setAccountType(event.target.value)}><option value="">Select account type</option><option value="personal">Personal</option><option value="business">Business</option></select>{accountType === "business" && (<input placeholder="Company name" />)}</div>);}
22. Controlled Components and Character Limits
function MessageBox() {const [message, setMessage] = useState("");function handleChange(event) {const value = event.target.value;if (value.length <= 200) {setMessage(value);}}return (<div><textarea value={message} onChange={handleChange} /><p>{message.length} / 200</p></div>);}
23. Transforming Input
function NameInput() {const [name, setName] = useState("");function handleChange(event) {setName(event.target.value.toUpperCase());}return (<input value={name} onChange={handleChange} />);}
Typing react becomes REACT. Be thoughtful — aggressive transforms can affect cursor position and UX.
24. Controlled Number Inputs
Even with type="number", event.target.value is generally a string. Convert intentionally when needed; keeping string state allows empty values naturally.
const [age, setAge] = useState("");const numericAge = Number(age);
25. Controlled Components and Validation State
const [form, setForm] = useState({email: "",password: ""});const [errors, setErrors] = useState({});{errors.email && <p>{errors.email}</p>}
Keep user input separate from validation messages.
26. Controlled Components and Loading State
const [isSubmitting, setIsSubmitting] = useState(false);async function handleSubmit(event) {event.preventDefault();setIsSubmitting(true);try {await saveForm(form);} finally {setIsSubmitting(false);}}<button type="submit" disabled={isSubmitting}>{isSubmitting ? "Saving..." : "Save"}</button>
Form data → Validation → Submission state → Result
27. Controlled vs Uncontrolled: Comparison
| Feature | Controlled | Uncontrolled |
|---|---|---|
| Source of truth | React state | DOM |
| Value access | State | Ref / DOM |
| value / onChange | Commonly used | Usually not / optional |
| Live validation | Easy | Less direct |
| Conditional UI | Easy | Less direct |
| Reset | Update state | DOM / reset methods |
| Complexity | Good for complex logic | Useful for simpler cases |
Neither is universally better — choose based on what the component needs.
28. When Controlled Components Are a Good Choice
- Live validation
- Value affects other UI
- Disable controls based on fields
- Reset fields via state
- Transform / normalize data
- Centralized multi-input form state
29. When an Uncontrolled Component May Be Useful
- You do not need to react to every keystroke
- The value is only needed at submission time
- The form is simple or ref-based access is more convenient
const inputRef = useRef(null);function handleSubmit(event) {event.preventDefault();console.log(inputRef.current.value);}
30. Common Mistake — Controlled Without onChange
// Read-only in practice<input value={name} />// Editable<inputvalue={name}onChange={(event) => setName(event.target.value)}/>
31. Common Mistake — Switching Modes
Avoid switching between value={undefined} and a string later. Initialize text inputs with "" so they are controlled from the start.
const [name, setName] = useState("");
32. Common Mistake — Mutating Form State
// Avoidform.email = "new@example.com";// InsteadsetForm((currentForm) => ({...currentForm,email: "new@example.com"}));
33. Common Mistake — Replacing the Entire Object
// Loses other fieldssetForm({ email: "new@example.com" });// Preserves other fieldssetForm((currentForm) => ({...currentForm,email: "new@example.com"}));
34. Common Mistake — Incorrect Checkbox Logic
// Bad for controlled checkbox<input type="checkbox" value={enabled} onChange={handleChange} />// Correct<inputtype="checkbox"checked={enabled}onChange={(event) => setEnabled(event.target.checked)}/>
35. Common Mistake — Overengineering
For a small form, separate state variables may be perfectly readable. Introduce abstractions when complexity justifies them.
const [name, setName] = useState("");const [email, setEmail] = useState("");
36. Practical Example — Registration Form
import { useState } from "react";function RegistrationForm() {const [form, setForm] = useState({name: "",email: "",password: ""});const [error, setError] = useState("");function handleChange(event) {const { name, value } = event.target;setForm((currentForm) => ({...currentForm,[name]: value}));}function handleSubmit(event) {event.preventDefault();if (!form.name.trim()) {setError("Name is required");return;}if (!form.email.trim()) {setError("Email is required");return;}if (!form.password) {setError("Password is required");return;}setError("");console.log("Registration:", form);}return (<form onSubmit={handleSubmit}><input id="name" name="name" value={form.name} onChange={handleChange} /><inputid="email"name="email"type="email"value={form.email}onChange={handleChange}/><inputid="password"name="password"type="password"value={form.password}onChange={handleChange}/>{error && <p>{error}</p>}<button type="submit">Register</button></form>);}
37. Practical Example — Profile Editor
function ProfileEditor() {const [profile, setProfile] = useState({name: "Rahul",email: "rahul@example.com",bio: "Frontend developer"});function handleChange(event) {const { name, value } = event.target;setProfile((currentProfile) => ({...currentProfile,[name]: value}));}function handleSave(event) {event.preventDefault();console.log("Updated profile:", profile);}return (<form onSubmit={handleSave}><input name="name" value={profile.name} onChange={handleChange} /><input name="email" value={profile.email} onChange={handleChange} /><textarea name="bio" value={profile.bio} onChange={handleChange} /><button type="submit">Save Profile</button></form>);}
38. Practical Example — Dynamic Form
function AccountForm() {const [accountType, setAccountType] = useState("");const [company, setCompany] = useState("");return (<form><selectvalue={accountType}onChange={(event) => setAccountType(event.target.value)}><option value="">Select</option><option value="personal">Personal</option><option value="business">Business</option></select>{accountType === "business" && (<inputvalue={company}onChange={(event) => setCompany(event.target.value)}placeholder="Company name"/>)}</form>);}
39. Controlled Components Mental Model
React State││ value / checked▼Form Element││ User interaction▼onChange│▼Event Handler│▼setState()│└───────────────┐▼React State
Text: state → value, event → setStateCheckbox: state → checked, event → setStateSelect: state → value, event → setState
40. Controlled Components in Real Applications
- Login — email/password → validation → submit
- Search — query → controlled input → filter
- Checkout — address/payment → validate → submit order
- Profile editing — load data → controlled inputs → save
- Settings — checkboxes/selects/toggles → preferences
41. Best Practices
- Keep React state as the source of truth (
value+onChange) - Initialize text inputs with
"" - Use immutable / functional updates
- Use
checkedfor checkboxes - Organize form state (separate vars vs object as clarity requires)
- Separate validation when it becomes complex
- Avoid unnecessary abstraction on simple forms
- Choose controlled or uncontrolled intentionally — do not mix accidentally
42. Interview Questions
1. What is a controlled component?
A form element whose value is controlled by React state.
2. What is the source of truth?
React state.
3. How does a controlled input work?
State provides value; onChange updates state.
4. value without onChange?
The input becomes effectively read-only unless other logic changes the value.
5. Controlled vs uncontrolled?
Controlled stores value in React state; uncontrolled lets the DOM hold it (often via refs).
6. Multiple controlled inputs?
function handleChange(event) {const { name, value } = event.target;setForm((currentForm) => ({...currentForm,[name]: value}));}
7. How do you control a checkbox?
checked={value} and event.target.checked.
8. How do you reset a controlled form?
setForm(initialForm);
9. Why not mutate form state?
Update through setters so React can track and render the update.
10. Must all forms be controlled?
No — choose based on requirements.
43. Practice Exercises
- Controlled name — display "You entered: …"
- Controlled email — error when empty
- Character counter — textarea with 200 limit (
45 / 200) - Checkbox — disable Submit until terms accepted
- Select — country selector; show selection below
- Multiple inputs — name, email, phone, city; one state object + one handler
- Registration — required fields, email format, password length, matching passwords
- Dynamic form — Business shows Company Name
- Edit profile — initial data for name/email/bio; edit and save
44. Quick Cheat Sheet
| Element | Controlled Property | Event Value |
|---|---|---|
| Text / email / password / number / textarea / select | value | event.target.value |
| Checkbox | checked | event.target.checked |
| Radio | checked | event.target.value |
| Form | — | onSubmit |
| Reset | State update | setForm(initialForm) |
Key Takeaway
A controlled component is a form element whose value is managed by React state.
const [value, setValue] = useState("");<inputvalue={value}onChange={(event) => setValue(event.target.value)}/>
User Input → onChange → setState() → React State → value / checked → Form Element
A controlled component is controlled by React state, not by the DOM alone.
That visibility makes validation, conditional rendering, resetting, submission, and dynamic form behavior much easier. Next: more complex form validation, custom inputs, reusable form components, and advanced form patterns.