Skip to content
Home JavaScript React Controlled Components — Checkbox Always True

React Controlled Components — Checkbox Always True

Where developers are forged. · Structured learning · Free forever.
📍 Part of: React.js → Topic 8 of 47
Checkbox always stores 'on' due to event.
⚙️ Intermediate — basic JavaScript knowledge assumed
In this tutorial, you'll learn
Checkbox always stores 'on' due to event.
  • A controlled input means React state IS the value — the DOM is just the display layer. If you can't derive the current input value from state alone, you have an uncontrolled input masquerading as one.
  • One state object + one generic handleChange that reads event.target.name scales to any size form — you almost never need a separate useState per field.
  • Checkboxes use event.target.checked (boolean), not event.target.value (always the string 'on'). Mixing these up creates a bug that won't throw an error — it'll just silently store wrong data.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
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.
🚨 START HERE

Controlled Component Quick Debug

Use these commands and checks to fix the most common controlled component issues fast.
🟡

Input not updating

Immediate ActionOpen React DevTools, select the component, check if state value changes on keystroke.
Commands
In browser console: `$r.state` or inspect the component state.
Add a `console.log(event.target.value)` inside onChange to verify events fire.
Fix NowEnsure onChange calls setState with the new value: `setState(event.target.value)`
🟡

Uncontrolled to controlled warning

Immediate ActionCheck state initialization in useState.
Commands
Search for `useState(undefined)` or `useState(null)` in your codebase.
Add a breakpoint at render and inspect the initial value prop on the input element.
Fix NowChange `useState(null)` to `useState('')` for string fields.
🟡

Checkbox always true

Immediate ActionCheck the onChange handler for the checkbox.
Commands
Log the event object: `console.log(event.target.checked, event.target.value)`
In DevTools, inspect the checked attribute on the DOM input.
Fix NowReplace `value` with `checked` in the handler: `setState(type === 'checkbox' ? checked : value)`
🟡

Form submits and reloads page

Immediate ActionCheck handleSubmit function.
Commands
Search all <form> elements that lack a submit handler with preventDefault.
Add a breakpoint in handleSubmit and step through.
Fix NowAdd `event.preventDefault()` as the first line in handleSubmit.
Production Incident

The Checkbox That Always Said Yes

A user's newsletter preference was always stored as 'on' — even when unchecked. The bug lived in production for three weeks before anyone noticed.
SymptomCheckbox value always updates to true regardless of user action.
Assumptionevent.target.value returns the checked state for checkboxes.
Root causeThe developer used event.target.value which always returns the string 'on' for checkboxes, instead of event.target.checked which returns the boolean.
FixIn the generic change handler, detect checkboxes by type and use checked: `` const { name, value, type, checked } = event.target; setFormValues(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value })); ``
Key Lesson
For checkboxes, always read event.target.checked not event.target.value.The string 'on' is truthy, so this bug silently corrupts data — no error is thrown.Every new engineer on the team must learn this: it's the #1 checkbox bug in React.
Production Debug Guide

Symptom → Action

Input value doesn't change when typing.Check that the input has both a value prop tied to state AND an onChange handler that calls the state setter with the new value.
React warning: 'A component is changing an uncontrolled input to be controlled'.Initialize the state variable to an empty string '' instead of undefined or null.
Checkbox always stores 'on' or true regardless of tick.Use event.target.checked for checkboxes, not event.target.value.
Select dropdown doesn't show the selected option.Ensure the value prop on <select> matches the value of one of the <option> elements.
Form submits and page reloads, losing all state.Add event.preventDefault() as the first line in your submit handler.

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 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.jsx · JAVASCRIPT
123456789101112131415161718192021222324252627282930
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)
🔥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.

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.jsx · JAVASCRIPT
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
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({\n    email: '',\n    password: '',\n    confirmPassword: '',\n  });

  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'

💡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 <select> elements, the controlled pattern looks identical to a text input: bind value to state and update via onChange. React handles matching the selected option automatically — you don't need to manually set selected on each <option> tag the way you'd do in plain HTML.

For <textarea>, React diverges from HTML. In HTML, textarea content goes between the tags. In React, you use the value prop, just like an input. This consistency is intentional — once you know the controlled pattern, it works the same way across all form elements.

PreferencesForm.jsx · JAVASCRIPT
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
import { useState } from 'react';

export default function PreferencesForm() {
  const [preferences, setPreferences] = useState({\n    newsletter: false,        // checkbox — boolean, not string\n    experienceLevel: 'junior', // select dropdown\n    bio: '',                  // textarea\n  });

  function handleChange(event) {
    const { name, value, type, checked } = event.target;

    setPreferences((prev) => ({\n      ...prev,\n      // For checkboxes use `checked`; for everything else use `value`\n      [name]: type === 'checkbox' ? checked : value,\n    }));
  }

  function handleSubmit(event) {
    event.preventDefault();
    console.log('Saved preferences:', preferences);
  }

  return (
    <form onSubmit={handleSubmit}>

      {/* CHECKBOX — bind to `checked`, not `value` */}
      <div>
        <label>
          <input
            type="checkbox"
            name="newsletter"
            checked={preferences.newsletter} // ← `checked` not `value`
            onChange={handleChange}
          />
          Subscribe to newsletter
        </label>
      </div>

      {/* SELECT — works exactly like a text input */}
      <div>
        <label htmlFor="experienceLevel">Experience Level</label>
        <select
          id="experienceLevel"
          name="experienceLevel"
          value={preferences.experienceLevel} // React auto-highlights the matching option
          onChange={handleChange}
        >
          <option value="junior">Junior (02 years)</option>
          <option value="mid">Mid (25 years)</option>
          <option value="senior">Senior (5+ years)</option>
        </select>
      </div>

      {/* TEXTAREA — use `value` prop, NOT children */}
      <div>
        <label htmlFor="bio">Short Bio</label>
        <textarea
          id="bio"
          name="bio"
          value={preferences.bio} // ← same pattern as input, not between tags
          onChange={handleChange}
          rows={4}
        />
      </div>

      <button type="submit">Save Preferences</button>
    </form>
  );
}
▶ Output
User ticks newsletter, selects 'mid', types 'I love React'.
On submit, console logs:
{\n newsletter: true,\n experienceLevel: 'mid',\n bio: 'I love React'\n}
⚠ Watch Out — Checkbox Value vs Checked:
Reading event.target.value on a checkbox always returns the string 'on', whether ticked or not. You'll store 'on' in state and your condition if (preferences.newsletter) will always be truthy. Always use event.target.checked for checkboxes.
📊 Production Insight
Mobile autocorrect can bypass onChange on some browsers for text inputs, but for controlled inputs it's impossible — React always overwrites the displayed value from state.
However, for checkboxes, the browser fires onChange reliably, but reading value instead of checked silently corrupts data.
Rule: always use event.target.checked for checkboxes; test with both ticked and unticked states in QA.
🎯 Key Takeaway
Checkboxes: use event.target.checked — always a boolean.
Select: same as text input — value prop driven by state.
Textarea: use value prop, not children.
Each form element has one consistent controlled pattern.

Controlled vs Uncontrolled — When Each Pattern Actually Makes Sense

Controlled components are the right default for most forms. But there are legitimate reasons to reach for uncontrolled inputs using useRef — and knowing when to make that call is what separates intermediate from senior React thinking.

The strongest case for uncontrolled inputs is file uploads. The file input's value can only be set by the user for security reasons — you can't programmatically assign it. So React simply cannot control it. You always use a ref and read ref.current.files on submit.

Another case is integrating with non-React libraries — things like rich text editors (Quill, Slate) or date pickers that manage their own internal DOM. Forcing a controlled wrapper around them often causes re-render conflicts. In those situations, uncontrolled refs are the pragmatic choice.

The mental model: if you need to read the value on submit only, uncontrolled is simpler. If you need the value continuously — for live validation, conditional rendering, or derived computations — controlled is the right tool. Don't use controlled components just because they're the 'React way'; use them because your feature genuinely needs that continuous sync.

FileUploadForm.jsx · JAVASCRIPT
12345678910111213141516171819202122232425262728293031323334353637
import { useRef } from 'react';

export default function FileUploadForm() {
  // useRef — we're NOT controlling this input, just reading it on submit
  const fileInputRef = useRef(null);

  function handleUploadSubmit(event) {
    event.preventDefault();

    // Access the files array directly from the DOM node via the ref
    const selectedFiles = fileInputRef.current.files;

    if (selectedFiles.length === 0) {
      alert('Please select a file first.');
      return;
    }

    const firstFile = selectedFiles[0];
    console.log('File name:', firstFile.name);
    console.log('File size (bytes):', firstFile.size);
    console.log('File type:', firstFile.type);

    // In production you'd send this to an API with FormData
  }

  return (
    <form onSubmit={handleUploadSubmit}>
      {/* File inputs CANNOT be controlled — use a ref instead */}
      <input
        type="file"
        ref={fileInputRef}
        accept="image/*"
      />
      <button type="submit">Upload</button>
    </form>
  );
}
▶ Output
User selects 'profile-photo.png' (42,500 bytes).
On submit:
File name: profile-photo.png
File size (bytes): 42500
File type: image/png
🔥Interview Gold — The File Input Exception:
Interviewers love asking 'can all inputs be controlled?' The answer is no: file inputs are read-only from JavaScript by design (a browser security rule). They must be handled with refs. Knowing this exception — and why it exists — signals solid understanding.
📊 Production Insight
Wrapping a third-party rich text editor as a controlled component caused infinite re-renders — the editor modified the DOM directly, which React detected and overwrote, triggering another editor update.
Fix: use an uncontrolled ref for the editor node and sync state only on blur or when the editor emits a change event.
Rule: controlled components are for React-owned state. If the DOM/library owns the state, don't try to control it — use refs and sync when necessary.
🎯 Key Takeaway
Controlled is the default for most inputs.
Uncontrolled is required for file inputs and safer for third-party DOM libraries.
Choose based on who owns the state: React or the DOM.

Scaling Forms with Custom Hooks and Dynamic Fields

Once you have the pattern of a single state object and a generic handler, the next step is extracting reusable form logic into a custom hook. A useForm hook can manage values, errors, touched state, and submission — and be shared across multiple forms in your app. This is the production pattern used by form libraries under the hood.

Dynamic forms — where users can add or remove sections (like multiple addresses or team members) — require extra care. Each dynamically added field needs a unique key to avoid React re-rendering bugs. Use a combination of a counter or UUID for keys, and store each item's data in an array indexed by those keys.

Custom hooks also help with async validation (checking username availability) and form-level actions like reset. Keep the hook pure — it should return state and handlers, not render anything.

For complex forms with deeply nested fields or hundreds of fields, consider a library like React Hook Form or Formik. But for 80% of forms, a well-built custom hook is cleaner and dependency-free.

useForm.js · JAVASCRIPT
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
import { useState, useCallback } from 'react';

export function useForm(initialValues, validate) {\n  const [values, setValues] = useState(initialValues);\n  const [errors, setErrors] = useState({});\n  const [touched, setTouched] = useState({});\n\n  const handleChange = useCallback((event) => {\n    const { name, value, type, checked } = event.target;
    setValues(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value,
    }));
    // Clear error when user starts typing
    setErrors(prev => ({ ...prev, [name]: '' }));
  }, []);

  const handleBlur = useCallback((event) => {
    const { name } = event.target;
    setTouched(prev => ({ ...prev, [name]: true }));
    if (validate) {
      const validationErrors = validate(values);
      setErrors(prev => ({ ...prev, ...validationErrors }));
    }
  }, [values, validate]);

  const handleSubmit = useCallback((callback) => (event) => {
    event.preventDefault();
    if (validate) {
      const validationErrors = validate(values);
      setErrors(validationErrors);
      if (Object.keys(validationErrors).length > 0) return;
    }
    callback(values);
  }, [values, validate]);

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  }, [initialValues]);

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
    reset,
  };
}
▶ Output
Usage in a component:
const { values, errors, handleChange, handleBlur, handleSubmit }
= useForm({ email: '', password: '' }, validateLogin);

<form onSubmit={handleSubmit(submitApi)}>
<input name="email" value={values.email} onChange={handleChange} onBlur={handleBlur} />
{errors.email && <span>{errors.email}</span>}
</form>
Mental Model
The Custom Hook Contract
A custom hook is a function that returns state and behavior — not JSX. Use it to encapsulate form logic so your components stay purely presentational.
  • Custom hooks can use other hooks (useState, useEffect, useCallback).
  • Return an object with values, errors, and handlers — never return JSX.
  • Keep the hook pure: no side effects like API calls inside; pass callbacks instead.
  • Dynamic fields need unique keys — use a counter or a library like nanoid.
  • Test the hook independently of the component — it's just a function.
📊 Production Insight
A dynamic form where each new section had the same index key caused React to reuse component instances, carrying over old input values.
Fix: use a unique identifier (like a timestamp or UUID) as the key when rendering dynamic field arrays.
Rule: for dynamic fields, never use array index as key if the list can be reordered or items added/removed.
🎯 Key Takeaway
Custom hooks make form logic reusable and testable.
Dynamic fields need unique keys — never use array index as key for mutable lists.
For 80% of forms, a custom hook is overkill — but for complex forms, it's a lifesaver.
🗂 Controlled vs Uncontrolled Components
A side-by-side comparison for production decisions
AspectControlled ComponentUncontrolled Component
Source of truthReact state (useState)The DOM itself
How you read the valuestateVariable directlyref.current.value on demand
Real-time validationTrivial — state updates on every keystrokeAwkward — requires DOM polling or event listeners
Conditional rendering based on inputNatural — state drives JSXFragile — requires manual DOM reads
File inputsNot possible (browser security)Required — use useRef
TestingEasy — pure state, no DOM neededHarder — requires simulating DOM events
Integration with 3rd-party UI libsCan cause re-render conflictsOften the safer choice
BoilerplateMore — needs value + onChange on every inputLess — just a ref

🎯 Key Takeaways

  • A controlled input means React state IS the value — the DOM is just the display layer. If you can't derive the current input value from state alone, you have an uncontrolled input masquerading as one.
  • One state object + one generic handleChange that reads event.target.name scales to any size form — you almost never need a separate useState per field.
  • Checkboxes use event.target.checked (boolean), not event.target.value (always the string 'on'). Mixing these up creates a bug that won't throw an error — it'll just silently store wrong data.
  • File inputs are the one place controlled components are architecturally impossible — browser security prevents JavaScript from setting a file input's value. Use useRef and read .files on submit.
  • Custom hooks encapsulate form logic for reuse. For dynamic fields, always use unique keys — never array index — to avoid stale rendering bugs.

⚠ Common Mistakes to Avoid

    Initializing form state as `null` or `undefined`
    Symptom

    React logs warning: 'A component is changing an uncontrolled input to be controlled'. Input behaves as uncontrolled initially, then becomes controlled after first keystroke.

    Fix

    Always initialize string fields as empty string '' instead of null or undefined.

    Omitting `event.preventDefault()` in submit handler
    Symptom

    Form performs a full page reload on submit, all state is lost, network shows unwanted GET/POST request.

    Fix

    Write event.preventDefault() as the first line of every handleSubmit function.

    Reading `event.target.value` inside async callback (setTimeout, fetch)
    Symptom

    event.target.value returns null or empty string because React recycles synthetic events.

    Fix

    Destructure the value immediately: const { value } = event.target; then use value in async code.

    Using array index as key for dynamic field lists
    Symptom

    Adding or removing items causes incorrect state retention — inputs display stale values.

    Fix

    Use a unique identifier (UUID, timestamp, or database ID) as the key for each dynamically rendered item.

    Not using `name` attribute in generic handler pattern
    Symptom

    All fields update the same state key or no field updates correctly.

    Fix

    Every input must have a name attribute that matches the key in your form state object.

Interview Questions on This Topic

  • QWhat is the difference between a controlled and an uncontrolled component in React, and how do you decide which one to use in a given situation?JuniorReveal
    A controlled component has its value managed by React state: the value prop is tied to state, and an onChange handler updates that state. The DOM is just a display layer. An uncontrolled component stores its own value in the DOM, and you read it using a ref when needed. When to choose: Use controlled for most inputs — you need live state for validation, conditional rendering, or formatting. Use uncontrolled for file inputs (cannot be controlled by design) and when integrating with third-party libraries that manage their own DOM (rich text editors, date pickers). Also, uncontrolled is simpler if you only need the value once, on submit. The rule of thumb: if you need the value continuously, go controlled. If you only need it at a specific moment, uncontrolled is fine.
  • QIf a user types into a controlled input but the displayed value doesn't change, what are the two most likely causes and how would you debug them?Mid-levelReveal
    Two most likely causes: 1. The onChange handler either doesn't exist or doesn't update state. The typical fix is to verify that onChange is bound and calls the state setter with the new value. Example: onChange={(e) => setValue(e.target.value)}. 2. The state setter is called but with a stale closure or incorrect value. For example, if you read event.target.value inside a setTimeout, the synthetic event may be recycled. Destructure immediately: const { value } = event.target; setValue(value); setTimeout(() => ...value...). Debugging steps: Open React DevTools and select the component. Watch how state changes as you type. If state updates but the input doesn't re-render, check if the value prop is actually bound to that state variable. Also add a console.log(event.target.value) inside the handler to confirm events fire correctly.
  • QWhy can't file inputs be controlled components in React, and what pattern do you use instead — and can you explain why that security restriction exists at the browser level?SeniorReveal
    File inputs cannot be controlled because the browser's security model prevents JavaScript from programmatically setting the value of a file input (e.g., input.value = '/path/to/file'). This is to protect against malicious websites that could read arbitrary files from the user's system without consent. Instead, you use an uncontrolled pattern: attach a ref to the file input with useRef, and on submit or user action, read ref.current.files to access the selected FileList object. The security restriction exists to prevent a script from silently accessing files — only the user's explicit action (selecting from the native file dialog) can populate the field. React acknowledges this by not allowing controlled file inputs at the framework level.

Frequently Asked Questions

What is a controlled component in React?

A controlled component is a form element — like an input, select, or textarea — whose displayed value is entirely managed by React state. You pass the current state value to the value prop and update state on every change via onChange. The DOM never holds truth independently of React.

Why does React warn 'changing an uncontrolled input to controlled'?

This warning fires when the value prop on an input switches from undefined (or null) to an actual string. React sees an uncontrolled input suddenly becoming controlled, which is a state management red flag. Fix it by always initialising your state as an empty string '' instead of undefined or null.

Do I need a form library like React Hook Form if I understand controlled components?

Not always. For small to medium forms with straightforward validation, plain controlled components with useState are clear, dependency-free, and easy to maintain. Libraries like React Hook Form, Formik, or Zod add real value when you have complex async validation, deeply nested field arrays, or need to minimise re-renders at scale — but they're overhead for simple use cases.

Can I use controlled components with third-party UI libraries like Material-UI or Ant Design?

Yes. Most modern UI libraries expose a value and onChange prop on their form components that align with the controlled pattern. For example, Material-UI's TextField accepts value and onChange exactly like native inputs. Check the library's documentation — if it exposes these props, the controlled pattern works the same way.

How do I handle resetting a controlled form?

Reset the form state object to its initial values using setFormValues(initialValues) and clear any errors or touched state. For convenience, extract the initial values as a constant outside the component or store them in a ref.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousReact RouterNext →React Context API
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged