Entradas Controladas vs No Controladas
En React 19, no necesitas escribir entradas completamente controladas manualmente para la mayoría de campos nativos. Pero los patrones controlados (especialmente a través de Controller) siguen siendo recomendados cuando trabajas con bibliotecas UI modernas o formularios interactivos ricos.
React Hook Form abraza componentes no controlados pero es completamente compatible con componentes controlados.
La mayoría de bibliotecas UI están construidas solo para modo controlado, por lo que mezclar es la forma estándar de obtener lo mejor de ambos mundos:
Rendimiento de campos nativos no controlados
Flexibilidad de componentes de biblioteca UI controlados
Controlada - El estado de React es la única fuente de verdad. Cada pulsación de tecla actualiza el estado y re-renderiza.
function ControlledInput () {
const [ email , setEmail ] = useState ( "" );
return (
< input
type = "email"
value = {email}
onChange = {( e ) => setEmail (e.target.value)}
/>
);
}
No controlada - El DOM sostiene el valor. Lo lees al enviar (a través de ref o FormData).
function UncontrolledInput () {
const ref = useRef < HTMLInputElement >( null );
function handleSubmit ( e : React . FormEvent ) {
e. preventDefault ();
console. log (ref.current?.value);
}
return (
< form onSubmit = {handleSubmit}>
< input type = "email" ref = {ref} defaultValue = "" />
< button type = "submit" >Enviar</ button >
</ form >
);
}
El estándar moderno para formularios simples - sin estado, sin refs, sin re-renderizados.
async function subscribe ( formData : FormData ) {
"use server" ;
const email = formData. get ( "email" );
await saveSubscriber (email);
}
export function NewsletterForm () {
return (
< form action = {subscribe}>
< input name = "email" type = "email" required />
< button type = "submit" >Suscribirse</ button >
</ form >
);
}
register() conecta las entradas como no controladas - rápido, re-renderizados mínimos.
import { useForm } from "react-hook-form" ;
type FormValues = { email : string ; password : string };
export function LoginForm () {
const { register , handleSubmit } = useForm < FormValues >();
return (
< form onSubmit = { handleSubmit (( data ) => console. log (data))}>
< input { ... register ( "email" )} type = "email" />
< input { ... register ( "password" )} type = "password" />
< button type = "submit" >Inicia sesión</ button >
</ form >
);
}
Usa Controller para conectar con componentes controlados como shadcn/ui Select o MUI.
import { Controller, useForm } from "react-hook-form" ;
import { Select, SelectTrigger, SelectContent, SelectItem } from "@/components/ui/select" ;
export function PlanForm () {
const { control , handleSubmit } = useForm ({ defaultValues: { plan: "free" } });
return (
< form onSubmit = { handleSubmit (( data ) => console. log (data))}>
< Controller
name = "plan"
control = {control}
render = {({ field }) => (
< Select value = {field.value} onValueChange = {field.onChange}>
< SelectTrigger />
< SelectContent >
< SelectItem value = "free" >Gratis</ SelectItem >
< SelectItem value = "pro" >Pro</ SelectItem >
</ SelectContent >
</ Select >
)}
/>
< button type = "submit" >Guardar</ button >
</ form >
);
}
La mayoría de <input>, <textarea>, <select> nativos → no controladas con register() o acciones del servidor
Componentes personalizados o de terceros (shadcn/ui, MUI, etc.) → controladas con <Controller>
Necesitas lógica en tiempo real en algunos campos, rendimiento en otros → mezcla según sea necesario
Nunca cambies una sola entrada de controlada a no controlada (o viceversa) durante su tiempo de vida - esto desencadena advertencias de React.
No añadas manualmente value + onChange a un campo que usa register() - rompe el patrón no controlado.
Para el mismo nombre de campo, mantén un enfoque consistente.
Siempre proporciona defaultValue (no value) para entradas no controladas para evitar la advertencia controlada/no controlada.
El formulario es simple (inicio de sesión, contacto, boletín, configuración básica - usualmente menos de 8 campos)
Principalmente necesitas los valores solo al enviar
Estás usando acciones del servidor con <form action={...}>
Quieres re-renderizados mínimos y el mejor rendimiento posible
Planeas usar useActionState y useFormStatus
La mejora progresiva importa (el formulario debe funcionar si JavaScript falla)
Estás bien confiando principalmente en validación HTML5 más validación del lado del servidor
Quieres el código más simple con reinicio automático del formulario en envío exitoso
No hay actualizaciones de UI complejas en tiempo real ni campos interdependientes
Prefieres menos repetición y un tamaño de bundle más pequeño
El formulario tiene muchos campos (más de 8–10) o es multi-paso
Necesitas validación en tiempo real, formato o retroalimentación en vivo mientras el usuario escribe
Los campos dependen entre sí (mostrar/ocultar, habilitar/deshabilitar o realizar cálculos)
Necesitas pre-poblar el formulario con datos de una API
Quieres mensajes de error del lado del cliente instantáneos con UX excelente
Tienes lógica compleja del lado del cliente antes del envío (fortaleza de contraseña, totales, vistas previas en vivo)
Estás usando TypeScript y quieres seguridad de tipo fuerte en todo el formulario
Necesitas control fino sobre las actualizaciones de UI
El formulario incluye cargas de archivos con vista previa o indicadores de progreso
Quieres mantener la mayoría de la lógica en el cliente antes de enviar datos al servidor
Formulario simple + Acciones del Servidor → Entradas no controladas con useActionState y useFormStatus
Complejo, multi-paso o UX rico → react-hook-form + zod (aún usa entradas no controladas internamente para rendimiento)
Editando datos existentes → Usualmente react-hook-form con reset() o requestFormReset nativo
Rendimiento máximo y código mínimo → Entradas no controladas nativas con hooks de React 19
Interactividad muy pesada en tiempo real → react-hook-form para el mejor balance de control y rendimiento
Comienza con entradas no controladas + hooks nativos de React 19 para cualquier formulario nuevo simple o de tamaño medio.
Cambia a react-hook-form en el momento en que necesites validación en tiempo real, campos condicionales o lógica compleja del lado del cliente.
Incluso cuando uses react-hook-form, todavía se beneficia del rendimiento de entrada no controlada bajo el capó.