Home JavaScript React Controlled Components — Checkbox Always True
Intermediate 13 min · March 05, 2026
React Forms and Controlled Components

React Controlled Components — Checkbox Always True

Checkbox always stores 'on' due to event.target.value — the #1 React bug.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Solid grasp of fundamentals
  • Comfortable reading code examples
  • Basic production concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • React state owns the input's value — the DOM just displays it.
  • Two requirements: value prop bound to state, and onChange handler that updates state.
  • Single form state object with generic handler scales to any number of fields.
  • Miss onChange and the input becomes read-only — React logs a warning in dev.
  • Common pitfall: starting state as null causes React to treat it as uncontrolled first.
✦ Definition~90s read
What is React Forms and Controlled Components?

A controlled component in React is one where the component's state is managed by React itself, not the DOM. When you write <input type="checkbox" checked={isChecked} onChange={handleChange} />, you're explicitly telling React: "I own this checkbox's state, and it will only change when I say so." This is the core contract of controlled components — the component's value is always driven by a React state variable, and any user interaction must flow through an event handler that updates that state.

Imagine a puppet show where the puppeteer controls every single movement of the puppet — the puppet never moves on its own.

The "checkbox always true" bug happens when developers violate this contract, typically by setting checked to a constant true or forgetting to wire up the onChange handler to toggle the state. Without that handler, React sees the state never change, so the checkbox stays stuck in its initial state — a classic footgun that trips up everyone from junior devs to seasoned engineers migrating from jQuery or vanilla JS.

Controlled components exist to solve the fundamental problem of "who owns the truth?" in React's declarative paradigm. In an uncontrolled input, the DOM owns the state — you read it via a ref or form submission. This works for simple cases but breaks down fast when you need validation, conditional rendering, or multi-field coordination.

Controlled inputs give you a single source of truth in your component's state, making it trivial to validate on every keystroke, disable a submit button until all fields are valid, or sync a checkbox with other UI elements. The trade-off is more boilerplate — every input needs its own state variable and handler — but libraries like Formik (used by 40%+ of React developers) and React Hook Form abstract this pattern while keeping the controlled contract intact.

Where controlled components shine is in complex forms with interdependent fields, real-time validation, or dynamic field arrays. Don't use them for simple, one-off forms where you just need to grab values on submit — uncontrolled inputs with ref or form data are faster to write and avoid unnecessary re-renders.

React's official docs recommend controlled components for most cases, but the ecosystem has evolved: React Hook Form uses uncontrolled inputs under the hood with refs for performance, then exposes a controlled-like API. The key insight is understanding the contract — once you internalize that React must be the sole source of truth for any controlled input, you'll stop fighting the framework and start building forms that behave predictably across checkboxes, selects, textareas, and custom components alike.

Plain-English First

Imagine a puppet show where the puppeteer controls every single movement of the puppet — the puppet never moves on its own. A controlled component in React is exactly that: React (the puppeteer) owns and controls the value inside every input field at all times. The input never gets to decide what it shows — React does. This is the opposite of an uncontrolled input, which is like a puppet that moves itself and you just check on it occasionally.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Forms are the front door of almost every real application. Login pages, checkout flows, search bars, profile editors — they all live and die by how reliably they handle user input. Get this wrong and you end up with out-of-sync UI, impossible-to-test logic, and bugs that only appear on slow networks or after a double-click. This is not a theoretical concern; it's the number one source of subtle bugs in junior-to-mid React codebases.

React's answer to this problem is the controlled component pattern. Before it existed, developers had to reach into the DOM with refs or query selectors to find out what a user typed — the same fragile approach that made jQuery apps notoriously hard to debug. Controlled components flip the model: the input's displayed value is always derived from React state, and every keystroke fires a handler that updates that state. The DOM is never the source of truth. React is.

By the end of this article you'll understand exactly why the controlled pattern was designed the way it was, how to build a fully validated multi-field form from scratch, how to handle edge cases like checkboxes and selects, and the specific mistakes that trip up developers who think they already know this stuff. You'll also walk away with the mental model that makes debugging any form issue fast and obvious.

Why Your Checkbox Stays True — The Controlled Component Contract

A controlled component in React is one where the form element's value is driven by component state, not the DOM. The input displays whatever the state says — always. The checkbox stays checked because you set checked={isChecked} and never update state on change. This is the core mechanic: React state is the single source of truth, and the DOM is just a reflection.

To make a controlled checkbox work, you must wire both checked and onChange. checked reads from state; onChange writes back. Miss either one and the checkbox becomes either read-only (no onChange) or uncontrolled (no checked prop). The common mistake: setting defaultChecked instead of checked. defaultChecked only sets the initial value — after that, React ignores it, and the DOM takes over. The checkbox appears to work until a re-render, then snaps back to the initial value.

Use controlled components when you need to validate, transform, or react to every keystroke or toggle — search inputs, multi-step forms, or any field whose value must be coordinated with other UI. Avoid them for simple, one-off forms where uncontrolled with a ref is simpler. The cost: every keystroke triggers a re-render. For high-frequency inputs (sliders, real-time search), debounce or switch to uncontrolled with refs.

⚠ The defaultChecked Trap
Using defaultChecked instead of checked makes the checkbox uncontrolled after mount — React will not update it on re-render, causing stale UI.
📊 Production Insight
A team shipped a settings panel where toggling a checkbox didn't persist after navigating away and back — they used defaultChecked and never wired onChange.
The symptom: checkbox appeared to toggle, but on re-mount it reverted to the initial value from props, confusing users and causing data loss.
Rule of thumb: if the value must survive re-renders or be read by other components, use checked + onChange — never defaultChecked.
🎯 Key Takeaway
Controlled components require both value/checked and onChange — one without the other breaks the contract.
defaultChecked is for uncontrolled components only; using it in a controlled pattern creates a silent bug.
Every controlled input re-renders on change — profile performance and debounce when necessary.
react-forms-controlled-components THECODEFORGE.IO Controlled Checkbox Lifecycle in React Step-by-step flow from state to DOM update Initialize State Set default checked value in useState Render Controlled Checkbox Pass checked prop from state User Clicks Checkbox onChange event fires with new value Update State Set new checked value via setState Re-render Component React updates DOM with new checked prop Checkbox Reflects State UI matches state, always true if state true ⚠ Missing onChange handler causes checkbox to stay true Always update state in onChange to reflect user input THECODEFORGE.IO
thecodeforge.io
React Forms Controlled Components

Why Uncontrolled Inputs Break Down (and What Controlled Inputs Fix)

In a plain HTML page, an input element owns its own value. You type, the DOM updates, and your JavaScript finds out by reading inputElement.value. This works fine for a static page, but React is built around one core rule: the UI is a function of state. If the DOM owns the value, React doesn't know about it — and the moment you try to do anything reactive (validation, formatting, conditional rendering based on what the user typed), you're fighting the framework.

Here's the concrete problem. Say you want to enforce that a username field is always lowercase. With an uncontrolled input, you'd have to intercept keystrokes, manipulate the DOM directly, and pray that mobile autocorrect doesn't bypass your handler. With a controlled input, it's one line: store the value as lowercase in state, and the input will always display lowercase — no DOM wrestling required.

The controlled pattern also makes testing dramatically easier. Because the form's entire state lives in plain JavaScript objects, you can test every validation rule without rendering a browser. That's the real payoff: React state is inspectable, serializable, and predictable. The DOM is none of those things.

UsernameInput.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useState } from 'react';

export default function UsernameInput() {
  // React owns the value — NOT the DOM
  const [username, setUsername] = useState('');

  function handleUsernameChange(event) {
    // We transform the value BEFORE storing it.
    // The input will always display what React has in state.
    const rawValue = event.target.value;
    const sanitised = rawValue.toLowerCase().replace(/\s/g, ''); // no spaces, always lowercase
    setUsername(sanitised);
  }

  return (
    <div>
      <label htmlFor="username">Username</label>
      {/* value={username} makes this a controlled input.
          Without this prop it becomes uncontrolled. */}
      <input
        id="username"
        type="text"
        value={username}          // ← React dictates what the input displays
        onChange={handleUsernameChange} // ← every keystroke goes through here
        placeholder="e.g. janedoe"
      />
      <p>Stored value: "{username}"</p>
    </div>
  );
}
Output
User types: "Jane Doe 123"
Stored value displayed below input: "janedoe123"
(Spaces are stripped and uppercase is converted in real time)
Try it live
🔥The Controlled Component Contract:
An input is controlled when two conditions are both true: (1) it has a value prop tied to state, AND (2) it has an onChange handler that updates that state. Miss either one and React will warn you — or worse, silently behave in confusing ways.
📊 Production Insight
A production bug: Username field that didn't normalize to lowercase caused duplicate user accounts across different email cases.
Fix: controlled input with onChange handler that transforms value before setState — guaranteed normalization at the source.
Rule: if you ever read the DOM for an input's value, you've lost React's reactivity. Use controlled components to keep the state as the single source of truth.
🎯 Key Takeaway
If you ever read the DOM for an input's value, you've lost React's reactivity.
Controlled inputs eliminate DOM reads entirely.
React state is the single source of truth for displayed values.

React Controlled Component Lifecycle Flow

Understanding the exact sequence of events in a controlled component is crucial for debugging. When a user types into an input, a chain reaction starts: the browser fires a synthetic event, React calls your onChange handler, you update state, and React triggers a re-render with the new value. The DOM then displays the updated value. This cycle repeats on every keystroke.

The diagram below illustrates the flow from user input to screen update. Notice that React sits between the user's action and the DOM update — that's exactly how it maintains control. If you break any link in this chain (missing onChange, stale closure, or direct DOM manipulation), the flow halts and the UI becomes out of sync.

💡Key Debugging Insight:
If the input does not update, check each link in this chain: Is the event firing? Is setState called with the correct value? Is the value prop bound to the state variable? React DevTools makes each step inspectable.
📊 Production Insight
A common production issue: the handler uses a stale closure capturing an old value of state. For example, using setState(state + 1) inside a useEffect without proper dependencies. The fix is to use the functional form setState(prev => prev + 1) to ensure you always have the latest state.
🎯 Key Takeaway
The controlled component lifecycle is a closed loop: input → event → handler → setState → re-render → updated input. Debug by isolating each step.
Controlled Component Lifecycle
onInput eventcalls onChange handlercalls setState with new valuetriggers re-rendernew value prop passedUser Types in InputReact captures SyntheticEventonChange function incomponentState updated with setStateComponent re-rendersInput element displays newvalue
react-forms-controlled-components THECODEFORGE.IO React Controlled Component Architecture Layered stack from state management to UI rendering State Layer useState | useReducer | Context Event Layer onChange | onClick | onSubmit Component Layer Checkbox | Select | Textarea Validation Layer Required | Pattern | Custom Rules Rendering Layer Virtual DOM | Reconciliation | DOM Update THECODEFORGE.IO
thecodeforge.io
React Forms Controlled Components

Building a Real Multi-Field Form with Validation

Most tutorials show a single input. Real apps have forms with five, ten, or twenty fields — and managing a separate useState call for each one becomes a maintenance nightmare fast. The cleaner production pattern is to store all field values in a single state object, use a generic change handler that reads the input's name attribute, and keep validation errors in a parallel object with matching keys.

This pattern scales because adding a new field means adding one key to your initial state object and one validation rule — nothing else changes. The handler and the error display logic are already written.

Validation belongs in a separate function that takes the current form values and returns an errors object. Keeping validation pure (no side effects, no DOM access) means you can unit test every rule in isolation. Only call this function on submit or on blur — validating on every keystroke is usually annoying for users unless you're checking something like password strength where live feedback adds value.

RegistrationForm.jsxJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import { useState } from 'react';

// Pure validation function — easy to unit test independently
function validateRegistration(fields) {
  const errors = {};

  if (!fields.email.includes('@')) {
    errors.email = 'Please enter a valid email address.';
  }

  if (fields.password.length < 8) {
    errors.password = 'Password must be at least 8 characters.';
  }

  if (fields.password !== fields.confirmPassword) {
    errors.confirmPassword = 'Passwords do not match.';
  }

  return errors; // empty object means no errors
}

export default function RegistrationForm() {
  // All field values in ONE state object — scales cleanly
  const [formValues, setFormValues] = useState({ email: '', password: '', confirmPassword: '' });

  const [errors, setErrors] = useState({});
  const [isSubmitted, setIsSubmitted] = useState(false);

  // Generic handler: reads event.target.name to know WHICH field changed
  function handleFieldChange(event) {
    const { name, value } = event.target;
    setFormValues((previousValues) => ({\n      ...previousValues,   // keep all other fields unchanged\n      [name]: value,       // update only the field that triggered the event\n    }));
  }

  function handleSubmit(event) {
    event.preventDefault(); // stop the browser reloading the page

    const validationErrors = validateRegistration(formValues);

    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors); // show errors, do NOT submit
      return;
    }

    // At this point the form is valid
    setErrors({});
    setIsSubmitted(true);
    console.log('Submitting registration:', formValues);
  }

  if (isSubmitted) {
    return <p>Thanks for registering, {formValues.email}!</p>;
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          name="email"          // ← MUST match the key in formValues
          value={formValues.email}
          onChange={handleFieldChange}
        />
        {/* Only show an error message if one exists for this field */}
        {errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
      </div>

      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          name="password"
          value={formValues.password}
          onChange={handleFieldChange}
        />
        {errors.password && <span style={{ color: 'red' }}>{errors.password}</span>}
      </div>

      <div>
        <label htmlFor="confirmPassword">Confirm Password</label>
        <input
          id="confirmPassword"
          type="password"
          name="confirmPassword"
          value={formValues.confirmPassword}
          onChange={handleFieldChange}
        />
        {errors.confirmPassword && (
          <span style={{ color: 'red' }}>{errors.confirmPassword}</span>
        )}
      </div>

      <button type="submit">Create Account</button>
    </form>
  );
}
Output
Scenario 1 — User submits with password 'abc' and no @ in email:
Error shown under email: "Please enter a valid email address."
Error shown under password: "Password must be at least 8 characters."
Form does NOT submit.
Scenario 2 — User fills in valid data:
Console: Submitting registration: { email: 'jane@example.com'
Try it live
🎯 Key Takeaway
Understanding building a real multi-field form with validation helps you build better React applications with cleaner, more maintainable code.

Deep Dive Section 4

Explore advanced patterns and best practices for controlled components in React forms. This section covers practical implementation details and edge cases.

💡Pro Tip — The name Attribute is the Bridge:
The name attribute on your input MUST exactly match the key in your state object for the generic handler pattern to work. A mismatch creates a new key instead of updating the right one — and React won't warn you about it. Name your inputs deliberately.
📊 Production Insight
On slow devices, validating every keystroke with expensive rules (like password strength) causes UI jank.
Fix: validate on blur or on submit, not on every change. Only validate live for fields that benefit from it.
Rule: pure validation functions are testable and predictable — keep them separate from component logic.
🎯 Key Takeaway
One state object + one generic handler = maintainable forms.
Adding a field means adding one key and one validation rule.
Validate on blur or submit, not every keystroke — unless the field needs live feedback.

Handling Checkboxes, Selects and Textarea — The Parts That Trip People Up

Text inputs are straightforward once you get the controlled pattern, but checkboxes, radio buttons, and select dropdowns each have a small twist that catches people out.

For checkboxes, the value you care about isn't event.target.value — it's event.target.checked, a boolean. If you accidentally read .value you'll get the string 'on' regardless of whether the box is ticked or not. That's a classic bug.

For