Controlled Components
Learn how controlled inputs let React state act as the single source of truth, using the value and onChange pattern for one or many form fields.
Introduction
In the previous lesson you saw that React supports two philosophies for handling form inputs. This lesson goes deep on the first one: controlled components, the approach most React applications reach for by default. In a controlled component, React state — not the DOM — decides exactly what appears inside an input at every moment.
You will learn why state is described as the "single source of truth" for a controlled input, the value + onChange pattern that makes it work, why every keystroke causes a re-render, and how to scale this pattern up to handle multiple fields with a single state object using computed property names.
- Why a controlled input's value is driven entirely by React state.
- The value + onChange pattern and how the two props work together.
- Why every keystroke triggers a re-render, and why that is not a performance problem.
- How to manage multiple form fields with a single state object.
- How to use computed property names like [e.target.name] to update just one field at a time.
State as the Single Source of Truth
Normally, an HTML input manages its own value internally — you type, and the browser updates what you see, entirely on its own. A controlled component turns that relationship around: the input's value is set directly from a React state variable, and the only way to change what is displayed is to update that state.
This means React state becomes the single source of truth: at any given moment, the input on screen simply reflects whatever the state currently holds. There is no separate, hidden copy of the value living inside the DOM that you need to keep in sync — state is the only place the data actually lives.
One Source of Truth
The input's displayed value always comes directly from React state, never from the DOM itself.
Fully Predictable
Given the current state, you always know exactly what the input shows — no guessing.
Easy to Reason About
Validating, resetting, or transforming input values is simple since it is all just state.
The value + onChange Pattern
To make an input controlled, you give it two props: value, set to a piece of state, and onChange, a handler that updates that state whenever the user types. Together they form a loop — state sets what is shown, and typing feeds back into state.
import { useState } from "react";
function ControlledInput() {
const [name, setName] = useState("");
function handleChange(event) {
setName(event.target.value);
}
return (
<div>
<input type="text" value={name} onChange={handleChange} />
<p>You typed: {name}</p>
</div>
);
}[ Sam ]
You typed: Samvalue={name} tells the input what to display. Typing fires onChange, which calls setName(event.target.value) with the new text. That state update causes a re-render, and the input's value prop is refreshed with the new name — completing the loop.
If you set value={name} but forget the onChange handler, the input becomes read-only from the user's perspective — React keeps resetting it back to the unchanging state value on every keystroke, and you will see a console warning about a missing onChange handler.
Why Every Keystroke Re-Renders
Because setName(...) is called on every single keystroke, the component re-renders every single keystroke too. This can sound wasteful at first, but it is exactly what makes controlled inputs so useful: on every render, you have instant, up-to-the-character access to the current value, which is what makes live validation, character counters, and conditional UI straightforward to build.
import { useState } from "react";
function LiveCharacterCount() {
const [bio, setBio] = useState("");
return (
<div>
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
maxLength={140}
/>
<p>{bio.length}/140 characters</p>
</div>
);
}[textarea with typed content]
23/140 charactersReact and the Virtual DOM are built for exactly this kind of frequent, small update. Re-rendering on every keystroke is the normal, expected cost of a controlled input, and it is virtually never a bottleneck for typical forms.
Managing Multiple Fields with One State Object
A real form usually has several fields — name, email, password, and so on. Instead of calling useState separately for every single field, a common pattern is to store all of them together in one state object, and write a single handleChange function that updates just the field that changed.
The key trick is giving each input a name attribute that matches a key in the state object, then using a computed property name — [event.target.name] — to update only that one property while spreading the rest of the previous state unchanged.
import { useState } from "react";
function SignupForm() {
const [formData, setFormData] = useState({
name: "",
email: "",
});
function handleChange(event) {
const { name, value } = event.target;
setFormData((prev) => ({
...prev,
[name]: value, // computed property name updates just this field
}));
}
function handleSubmit(event) {
event.preventDefault();
console.log(formData);
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
/>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
<button type="submit">Sign Up</button>
</form>
);
}{ name: "Aria", email: "aria@mail.com" }Wrapping event.target.name in square brackets tells JavaScript to use the string it evaluates to ("name" or "email") as the actual property key, so the exact same handleChange function can update any field just by reading its name attribute.
Common Mistakes
- Setting value on an input but forgetting onChange, which freezes the input and triggers a console warning.
- Forgetting to spread the previous state (...prev) when updating one field, which erases every other field.
- Mismatching an input's name attribute with the state object's key, so [event.target.name] updates the wrong (or a new, unintended) property.
- Initializing state as undefined instead of an empty string, which can make an input silently switch between controlled and uncontrolled.
Best Practices
- Always pair value with onChange on a controlled input — never one without the other.
- Initialize state for text inputs with an empty string, not undefined, to avoid controlled/uncontrolled warnings.
- Use one state object plus a single handleChange for forms with several related fields.
- Match each input's name attribute exactly to its key in the state object.
- Always spread the previous state (...prev) when updating just one field, to keep the rest intact.
Frequently Asked Questions
Why is it called a "controlled" component?
Because React state fully controls what value the input displays — the DOM never manages the value independently, it just reflects whatever state currently holds.
Does re-rendering on every keystroke hurt performance?
In virtually all real-world forms, no. React is designed to handle frequent small updates efficiently, and the benefits of instant access to the current value far outweigh the cost.
Can one handleChange function really work for every field?
Yes, as long as each input has a name attribute matching a key in your state object, the same handler can update any of them using event.target.name and event.target.value.
What happens if I forget to spread the previous state?
Only the field you explicitly set will remain in the object — every other field gets wiped out, since you replaced the whole state object instead of merging into it.
Is a controlled component always the better choice?
It is the more common default because of the extra control it gives you, but it is not strictly required — the next lesson covers uncontrolled components, which can be simpler for very basic forms.
Key Takeaways
- In a controlled component, React state is the single source of truth for an input's value.
- The value + onChange pair is what makes an input controlled — value displays state, onChange updates it.
- Every keystroke updates state and re-renders the component, giving instant access to the latest value.
- Multiple fields can share one state object, updated by a single handleChange function.
- Computed property names like [event.target.name]: event.target.value update only the field that changed.
Summary
Controlled components put React state firmly in charge of every form input, using the simple but powerful value + onChange pattern. Scaling this up to a whole form is just a matter of storing all the fields in one state object and using computed property names to update exactly the field that changed.
In this lesson, you learned why controlled inputs treat state as the single source of truth, how the value + onChange loop works, why re-rendering on every keystroke is expected and fine, and how to manage several fields with one state object. Next, you will learn the alternative approach: uncontrolled components, where the DOM manages input values instead of React.
- You understand why controlled inputs treat state as the single source of truth.
- You can wire up the value + onChange pattern correctly.
- You know why controlled inputs re-render on every keystroke, and why that is fine.
- You can manage multiple fields with one state object using computed property names.