¿Cuál es la diferencia entre un componente controlado y uno no controlado en React?
Ver respuesta — intenta responderla en voz alta primero
En un componente controlado, React es la fuente de la verdad: el input recibe su value del estado y lo actualiza con onChange. En uno no controlado, el DOM guarda el valor y tú lo lees cuando lo necesitas, normalmente con una ref.
Un input HTML normal guarda su propio valor internamente. React te deja elegir quién manda sobre ese valor.
- Controlado: pasas
valuedesde un estado y actualizas cononChange. Cada tecla pasa por React, así que el estado y la pantalla siempre coinciden. - No controlado: usas
defaultValuepara el valor inicial y una ref para leer el valor del DOM cuando haga falta (por ejemplo, al enviar el formulario).
Ventajas de cada uno:
- Controlado: validación en vivo, transformar lo que se escribe, habilitar o deshabilitar botones según el valor. Es la opción por defecto en formularios con lógica.
- No controlado: menos código y ningún re-render por tecla. Útil en formularios simples donde solo importa el valor al final, o al integrar librerías que manipulan el DOM.
import { useRef, useState, type FormEvent } from "react";
export function Formulario() {
// CONTROLADO: React es la fuente de la verdad del valor
const [correo, setCorreo] = useState("");
// NO CONTROLADO: el DOM guarda el valor; lo leemos con una 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}>
{/* Controlado: value + onChange, React vigila cada tecla */}
<input
value={correo}
onChange={(evento) => setCorreo(evento.target.value)}
placeholder="correo"
/>
{/* Validación en vivo, posible gracias al estado */}
{correo !== "" && !correo.includes("@") && <p>Correo inválido</p>}
{/* No controlado: defaultValue + ref, React no lo vigila */}
<input ref={nombreRef} defaultValue="Ana" placeholder="nombre" />
<button type="submit">Enviar</button>
</form>
);
}
Mezclar los dos mundos: poner value sin onChange. El input queda congelado (React repinta siempre el mismo valor) y la consola muestra un warning.
// MAL: value sin onChange, el usuario escribe y no pasa nada
<input value={correo} />
// BIEN: o lo controlas completo...
<input value={correo} onChange={(e) => setCorreo(e.target.value)} />
// ...o solo quieres un valor inicial y usas defaultValue
<input defaultValue="hola" />
La regla: value siempre va acompañado de onChange; si solo necesitas el valor inicial, eso es defaultValue.
Un componente controlado toma su valor del estado de React con
valuey lo actualiza cononChange, así que React es la fuente de la verdad. Uno no controlado deja el valor en el DOM y yo lo leo con una ref, normalmente al enviar el formulario. En formularios con validación en vivo o lógica que depende de lo escrito, prefiero controlados. Si el formulario es simple y solo necesito el valor al final, uno no controlado me ahorra estado y re-renders. Y nunca mezclo:valuesinonChangedeja el input congelado.
Crea un input controlado CampoUsuario que muestre cuántos caracteres lleva escritos y deshabilite el botón "Guardar" si está vacío.
Ver solución
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"
/>
{/* Ambas cosas se derivan del estado: por eso conviene controlarlo */}
<p>{usuario.length} caracteres</p>
<button disabled={usuario === ""}>Guardar</button>
</div>
);
}