React Controlled vs Uncontrolled Components Explained

When a React input should be controlled with state, and when the browser should keep the value. useState, refs, FormData and form actions.

By CrunchCubeUpdated 21 October 20265 min read

React developers often learn one rule early: "a form input should be controlled". Then they assume every field needs its own state variable, and a ten-field form turns into ten useState calls and ten onChange handlers.

That rule is only half the story. Let's look at both kinds of input, what each one is good at, and how to choose.

The simple idea: who holds the pen?

Imagine a form is a sheet of paper. The question is who holds the pen.

  • Controlled: React holds the pen. The browser shows what React says, and every key you press is a request: "please change the value to this". React decides.
  • Uncontrolled: The browser holds the pen. You type freely, and React only reads the sheet when it needs it, usually on submit.

A controlled input

import { useState } from "react";
 
function NameField() {
  const [name, setName] = useState("");
 
  return (
    <label>
      Name
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <p>Hello, {name || "stranger"}</p>
    </label>
  );
}

Two things make it controlled: value={name} (React decides what's shown) and onChange (React updates its state on every keystroke). The greeting updates live because the value lives in React.

Because every change passes through your code, you can reshape it on the way in:

<input
  value={code}
  onChange={(e) => setCode(e.target.value.toUpperCase().slice(0, 6))}
/>

The user types "abc123xyz", and the box shows "ABC123". Try doing that cleanly with an uncontrolled input.

An uncontrolled input

import { useRef } from "react";
 
function NewsletterForm() {
  const emailRef = useRef(null);
 
  function handleSubmit(e) {
    e.preventDefault();
    console.log("Subscribing", emailRef.current.value);
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} type="email" defaultValue="" required />
      <button>Subscribe</button>
    </form>
  );
}

No state, no onChange. defaultValue sets the starting text once, and after that the browser owns it. We read it from the ref only when the form is submitted.

For forms with many fields, you don't even need refs. The browser already collects everything:

function ContactForm() {
  function handleSubmit(e) {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.currentTarget));
    console.log(data); // { name: "...", email: "...", message: "..." }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input name="name" />
      <input name="email" type="email" />
      <textarea name="message" />
      <button>Send</button>
    </form>
  );
}

Three fields, zero state. The name attribute is the key in the resulting object.

React 19 form actions

React 19 made this pattern first-class. You can pass a function to the form's action, and React calls it with the FormData:

function ContactForm() {
  async function send(formData) {
    await fetch("/api/contact", { method: "POST", body: formData });
  }
 
  return (
    <form action={send}>
      <input name="email" type="email" required />
      <button>Send</button>
    </form>
  );
}

React also resets uncontrolled fields after a successful action, which is exactly what you want for a contact form.

Which one should you use?

Think of Federer's racket and the ball machine at practice. The ball machine fires the same ball every time, no thinking required. A match rally needs reaction to every shot. Use the simple tool when nothing needs to react, and the responsive one when something does.

You need to...Choose
Show live feedback (character count, preview, validation as you type)Controlled
Change the value as it's typed (uppercase, masks, limits)Controlled
Enable the submit button only when fields are validControlled
Let one field's value change another fieldControlled
Just collect values on submitUncontrolled
Upload files (<input type="file">)Uncontrolled (always)
Build a large form where typing must stay fastUncontrolled, or a library like React Hook Form

File inputs are the one hard rule: their value is read-only for security reasons, so they're always uncontrolled.

What goes wrong

The famous warning

A component is changing an uncontrolled input to be controlled.

This happens when value starts as undefined and later becomes a string:

const [name, setName] = useState();          // undefined: uncontrolled at first
<input value={name} onChange={...} />        // becomes controlled after typing

React sees value={undefined} as "no value prop", so the input begins uncontrolled. Fix it by starting with an empty string: useState(""). If data arrives from an API, use value={user?.name ?? ""}.

A frozen input

<input value={name} />  // no onChange

You set value but never update it, so React keeps resetting the box to the same text. Typing does nothing. React warns about this too. Add onChange, or use defaultValue if you meant uncontrolled.

Mixing both on one input

Passing both value and defaultValue is a contradiction: "React holds the pen" and "the browser holds the pen" at once. Pick one.

Performance, honestly

A controlled input re-renders its component on every keystroke. For a typical form that's completely fine; React handles it easily. It becomes a problem when a huge component re-renders, for example a form inside a page with a big table. Then either move the field into its own small component (so only it re-renders) or switch to uncontrolled inputs.

That's the reason React Hook Form is built mostly on uncontrolled inputs: it reads values with refs and only re-renders when it needs to show something, like an error.

Next, see how JavaScript closures behave inside React handlers, or learn who should own your data: state, props or context.

In one line: control an input when React needs to react to every keystroke, and leave it uncontrolled when you only need the value at the end.

Keep reading

New games, tools and tutorials

About one short email a week. No spam, one-click unsubscribe.