← React & TypeScript

Estado y patrones

¿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 value desde un estado y actualizas con onChange. Cada tecla pasa por React, así que el estado y la pantalla siempre coinciden.
  • No controlado: usas defaultValue para 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 value y lo actualiza con onChange, 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: value sin onChange deja el input congelado.

Reto rápido

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>
  );
}