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
valuefrom state and update withonChange. Every keystroke goes through React, so state and screen always match. - Uncontrolled: you use
defaultValuefor 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
valueand updates it withonChange, 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:valuewithoutonChangeleaves the input frozen.
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>
);
}