Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Controlado - el padre es propietario del estado
function ControlledInput() {
const [value, setValue] = useState("");
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
// No controlado - el DOM es propietario del estado
function UncontrolledInput() {
const ref = useRef<HTMLInputElement>(null);
const handleSubmit = () => console.log(ref.current?.value);
return <input ref={ref} defaultValue="" />;
}
// Flexible - soporta ambos modos
function FlexibleInput({
value: controlledValue,
defaultValue = "",
onChange,
}: {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
}) {
const [internalValue, setInternalValue] = useState(defaultValue);
const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isControlled) setInternalValue(e.target.value);
onChange?.(e.target.value);
};
return <input value={value} onChange={handleChange} />;
}Cuándo usarlo: Cada componente interactivo debe decidir quién es propietario de su estado. Usa controlado cuando el padre necesita leer o modificar el valor. Usa no controlado para casos más simples donde el componente se gestiona a sí mismo. Construye componentes de biblioteca que soporten ambos modos.
import { useState, useRef, useCallback, type ReactNode } from "react";
// Un componente Toggle que soporta uso controlado y no controlado
interface ToggleProps {
pressed?: boolean;
defaultPressed?: boolean;
onPressedChange?: (pressed: boolean) => void;
children: ReactNode;
}
function Toggle({
pressed: controlledPressed,
defaultPressed = false,
onPressedChange,
children,
}: ToggleProps) {
const [internalPressed, setInternalPressed] = useState(defaultPressed);
const isControlled = controlledPressed !== undefined;
const pressed = isControlled ? controlledPressed : internalPressed;
const handleClick = useCallback(() => {
const next = !pressed;
if (!isControlled) {
setInternalPressed(next);
}
onPressedChange?.(next);
}, [pressed, isControlled, onPressedChange]);
return (
<button
type="button"
role="switch"
aria-checked={pressed}
onClick={handleClick}
className={`px-4 py-2 rounded-full transition-colors ${
pressed
? "bg-blue-600 text-white"
: "bg-gray-200 text-gray-700"
}`}
>
{children}
</button>
);
}
// --- Ejemplos de uso ---
// No controlado - el componente gestiona su propio estado
function SimpleToggle() {
return (
<Toggle
defaultPressed={false}
onPressedChange={(p) => console.log("Alternado:", p)}
>
Modo Oscuro
</Toggle>
);
}
// Controlado - el padre es propietario y puede sobreescribir el estado
function SyncedToggles() {
const [enabled, setEnabled] = useState(false);
return (
<div className="flex gap-4">
<Toggle pressed={enabled} onPressedChange={setEnabled}>
Alternar A
</Toggle>
<Toggle pressed={enabled} onPressedChange={setEnabled}>
Alternar B
</Toggle>
<p>Ambos están: {enabled ? "ENCENDIDOS" : "APAGADOS"}</p>
</div>
);
}Lo que esto demuestra:
isControlled determina qué fuente de estado usardefaultPressed para el estado inicialdefault* prop (p. ej., defaultValue, defaultChecked, defaultPressed) señala el estado inicial no controlado.undefined.useActionState pueden simplificar patrones de formularios, pero la distinción controlada/no controlada aún se aplica.| Convención de Prop | Modo | Propósito |
|---|---|---|
value | Controlado | Valor actual, establecido por el padre |
defaultValue | No Controlado | Valor inicial, el componente gestiona después |
onChange | Ambos | Callback que notifica al padre de cambios |
ref | No Controlado | Acceso imperativo para leer el valor del DOM |
Hook useControllableState - extrae el patrón en un hook reutilizable:
function useControllableState<T>({
value: controlledValue,
defaultValue,
onChange,
}: {
value?: T;
defaultValue: T;
onChange?: (value: T) => void;
}): [T, (next: T) => void] {
const [internalValue, setInternalValue] = useState(defaultValue);
const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;
const setValue = useCallback(
(next: T) => {
if (!isControlled) setInternalValue(next);
onChange?.(next);
},
[isControlled, onChange]
);
return [value, setValue];
}
// Uso dentro de cualquier componente
function Slider({ value, defaultValue = 0, onChange, min = 0, max = 100 }: SliderProps) {
const [current, setCurrent] = useControllableState({
value,
defaultValue,
onChange,
});
// ... renderiza con `current` y `setCurrent`
}React 19 form actions - formularios no controlados con acciones de servidor:
function ContactForm() {
async function submitAction(formData: FormData) {
"use server";
const email = formData.get("email") as string;
await sendEmail(email);
}
return (
<form action={submitAction}>
<input name="email" type="email" defaultValue="" />
<button type="submit">Enviar</button>
</form>
);
}value?: T) para la prop controlada para que undefined señale modo no controlado.defaultValue obligatorio cuando value no se proporciona, o dale un valor por defecto razonable.Cambiar entre controlado y no controlado - Cambiar value de undefined a un valor definido (o viceversa) durante el ciclo de vida del componente causa bugs. React advierte sobre esto. Solución: Decide el modo en el montaje y adhiérete a él. Usa un ref para rastrear el modo inicial.
Entrada controlada con actualización de estado retrasada - Si el manejador onChange actualiza el estado de forma asincrónica (p. ej., con debounce), la entrada parece congelarse. Solución: Actualiza el estado local inmediatamente y aplica debounce solo al efecto secundario, no a la actualización de estado.
onChange faltante en componente controlado - Proporcionar value sin onChange crea una entrada de solo lectura. React advierte. Solución: Siempre empareja value con onChange, o usa readOnly si es intencional.
defaultValue cambia después del montaje - Cambiar defaultValue después del primer renderizado no tiene efecto. Solución: Usa una prop key para remontar el componente si el valor inicial necesita ser restablecido.
| Enfoque | Compensación |
|---|---|
| Controlado | Control completo del padre; requiere gestión de estado en el padre |
| No Controlado | Más simple; más difícil para el padre leer o sincronizar estado |
| Flexible (ambos modos) | Mejor para bibliotecas; más complejidad de implementación |
| React 19 form actions | Excelente para formularios; no controlado con manejo del lado del servidor |
| Biblioteca de gestión de estado | Zustand o Redux pueden actuar como el controlador para formularios complejos |
value + onChange). El padre es la única fuente de verdad.ref o recibir notificaciones a través de callbacks.default* señala el estado inicial no controlado.const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;value) es undefined.undefined, usa el estado interno.[value, setValue] y maneja el estado interno, passthrough controlado, y callbacks onChange.<form action={submitAction}>
<input name="email" type="email" defaultValue="" />
<button type="submit">Enviar</button>
</form>defaultValue y leen valores desde FormData.value de undefined a un valor definido (o viceversa) causa bugs y advertencias de React.onChange actualiza el estado de forma asincrónica (p. ej., con debounce), la prop value de la entrada no se actualiza inmediatamente.interface ToggleProps {
pressed?: boolean; // controlado
defaultPressed?: boolean; // no controlado
onPressedChange?: (pressed: boolean) => void;
children: ReactNode;
}pressed?: boolean) para que undefined señale modo no controlado.defaultPressed un valor por defecto razonable.value como defaultValue.defaultValue solo establece el estado inicial en el primer renderizado.defaultValue en renderizados posteriores porque el estado interno ya está inicializado.key para remontar el componente si el valor inicial necesita ser restablecido.value en cada renderizado.onChange faltante.value con onChange, o añade explícitamente el atributo readOnly si es intencional.Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥