LearnContact
Lesson 2024 min read

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.

What You Will Learn
  • 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.

ControlledInput.jsx
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>
  );
}
Rendered Output (after typing "Sam")
[ Sam ]
You typed: Sam
The Loop, Step by Step

value={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.

A Frozen Input

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.

LiveCharacterCount.jsx
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>
  );
}
Rendered Output (as the user types)
[textarea with typed content]
23/140 characters
Not a Performance Concern in Practice

React 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.

SignupForm.jsx
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>
  );
}
Console (after typing "Aria" / "aria@mail.com" and submitting)
{ name: "Aria", email: "aria@mail.com" }
Why [name]: value Works

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

Avoid These 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.

Lesson 20 Completed
  • 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.
Next Lesson →

Uncontrolled Components