← React & TypeScript

State & Patterns

What is the difference between a controlled and an uncontrolled component in React?

Show answer — try answering out loud first

In a controlled component, React is the source of truth: the input gets its value from state and updates it with onChange. In an uncontrolled one, the DOM holds the value and you read it when you need it, usually with a ref.

A normal HTML input stores its own value internally. React lets you choose who's in charge of that value.

  • Controlled: you pass value from state and update with onChange. Every keystroke goes through React, so state and screen always match.
  • Uncontrolled: you use defaultValue for the initial value and a ref to read the DOM value when needed (for example, on form submit).

Advantages of each:

  • Controlled: live validation, transforming what's typed, enabling or disabling buttons based on the value. It's the default choice for forms with logic.
  • Uncontrolled: less code and no re-render per keystroke. Useful in simple forms where only the final value matters, or when integrating libraries that manipulate the DOM.
import { useRef, useState, type FormEvent } from "react";

export function Formulario() {
  // CONTROLLED: React is the source of truth for the value
  const [correo, setCorreo] = useState("");

  // UNCONTROLLED: the DOM holds the value; we read it with a ref
  const nombreRef = useRef<HTMLInputElement>(null);

  function manejarEnvio(evento: FormEvent<HTMLFormElement>) {
    evento.preventDefault();
    console.log("Correo (desde el estado):", correo);
    console.log("Nombre (desde el DOM):", nombreRef.current?.value);
  }

  return (
    <form onSubmit={manejarEnvio}>
      {/* Controlled: value + onChange, React watches every keystroke */}
      <input
        value={correo}
        onChange={(evento) => setCorreo(evento.target.value)}
        placeholder="correo"
      />
      {/* Live validation, possible thanks to state */}
      {correo !== "" && !correo.includes("@") && <p>Correo inválido</p>}

      {/* Uncontrolled: defaultValue + ref, React doesn't watch it */}
      <input ref={nombreRef} defaultValue="Ana" placeholder="nombre" />

      <button type="submit">Enviar</button>
    </form>
  );
}

Mixing the two worlds: setting value without onChange. The input freezes (React always repaints the same value) and the console shows a warning.

// BAD: value without onChange, the user types and nothing happens
<input value={correo} />

// GOOD: either you control it fully...
<input value={correo} onChange={(e) => setCorreo(e.target.value)} />

// ...or you just want an initial value and use defaultValue
<input defaultValue="hola" />

The rule: value always goes with onChange; if you only need the initial value, that's defaultValue.

A controlled component takes its value from React state with value and updates it with onChange, so React is the source of truth. An uncontrolled one leaves the value in the DOM and I read it with a ref, usually on form submit. For forms with live validation or logic that depends on what's typed, I prefer controlled ones. If the form is simple and I only need the value at the end, an uncontrolled one saves me state and re-renders. And I never mix them: value without onChange leaves the input frozen.

Quick challenge

Create a controlled input CampoUsuario that shows how many characters have been typed and disables the "Save" button if it's empty.

See solution
import { useState } from "react";

export function CampoUsuario() {
  const [usuario, setUsuario] = useState("");

  return (
    <div>
      <input
        value={usuario}
        onChange={(evento) => setUsuario(evento.target.value)}
        placeholder="nombre de usuario"
      />
      {/* Both things derive from state: that's why it's worth controlling it */}
      <p>{usuario.length} caracteres</p>
      <button disabled={usuario === ""}>Guardar</button>
    </div>
  );
}