En esta página
Formularios Controlados
Formularios controlados vs no controlados
En React existen dos enfoques para manejar formularios:
Controlados: React controla el valor del campo en cada momento mediante estado. El elemento es la fuente de verdad en React.
function Controlado() {
const [valor, setValor] = useState('');
// React siempre sabe el valor actual
return <input value={valor} onChange={(e) => setValor(e.target.value)} />;
}No controlados: El DOM gestiona el valor. React accede al valor solo cuando es necesario (en el submit).
function NoControlado() {
const inputRef = useRef<HTMLInputElement>(null);
const manejarSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log(inputRef.current?.value); // Solo al enviar
};
return <form onSubmit={manejarSubmit}><input ref={inputRef} /></form>;
}Cuándo usar cada uno:
| Característica | Controlado | No controlado |
|---|---|---|
| Validación en tiempo real | ✅ | ❌ |
| Valores derivados | ✅ | ❌ |
| Rendimiento (muchos campos) | Peor | Mejor |
| Simplicidad de código | Media | Alta |
| Integración con librerías externas | Difícil | Fácil |
Patrón de formulario con objeto de estado
Para formularios con múltiples campos, un único objeto de estado es más manejable:
interface DatosFormulario {
nombre: string;
apellido: string;
email: string;
telefono: string;
rol: string;
}
function FormularioPerfil(): React.JSX.Element {
const [form, setForm] = useState<DatosFormulario>({
nombre: '', apellido: '', email: '', telefono: '', rol: 'usuario',
});
// Handler genérico que funciona con cualquier campo
const actualizar = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
};
return (
<form>
<input name="nombre" value={form.nombre} onChange={actualizar} />
<input name="apellido" value={form.apellido} onChange={actualizar} />
<input name="email" type="email" value={form.email} onChange={actualizar} />
<select name="rol" value={form.rol} onChange={actualizar}>
<option value="usuario">Usuario</option>
<option value="editor">Editor</option>
<option value="admin">Administrador</option>
</select>
</form>
);
}Validación avanzada con esquemas
Para proyectos más grandes, valida con Zod:
import { z } from 'zod';
const esquemaRegistro = z.object({
nombre: z.string().min(2, 'Mínimo 2 caracteres').max(50),
email: z.string().email('Email inválido'),
password: z.string().min(8, 'Mínimo 8 caracteres').regex(
/[A-Z]/, 'Debe contener al menos una mayúscula'
),
edad: z.number().int().min(13, 'Debes tener al menos 13 años'),
});
type DatosRegistro = z.infer<typeof esquemaRegistro>;
function validarConZod(datos: unknown): { valido: boolean; errores: Record<string, string> } {
const resultado = esquemaRegistro.safeParse(datos);
if (resultado.success) return { valido: true, errores: {} };
const errores: Record<string, string> = {};
resultado.error.issues.forEach((issue) => {
const campo = issue.path[0] as string;
errores[campo] = issue.message;
});
return { valido: false, errores };
}React 19 useActionState — formularios del futuro
useActionState es el hook de React 19 para manejar el estado de formularios con actions asíncronas:
import { useActionState } from 'react';
interface Estado {
campo?: string;
error?: string;
}
async function accionBusqueda(_prev: Estado, formData: FormData): Promise<Estado> {
const termino = formData.get('termino') as string;
if (!termino.trim()) return { error: 'Ingresa un término de búsqueda' };
const res = await fetch(`/api/buscar?q=${encodeURIComponent(termino)}`);
if (!res.ok) return { error: 'Error en la búsqueda' };
return { campo: termino };
}
function BuscadorConAction(): React.JSX.Element {
const [estado, accion, pending] = useActionState(accionBusqueda, {});
return (
<form action={accion}>
<input name="termino" type="search" placeholder="Buscar…" />
<button type="submit" disabled={pending}>
{pending ? 'Buscando…' : 'Buscar'}
</button>
{estado.error && <p role="alert">{estado.error}</p>}
{estado.campo && <p>Resultados para: {estado.campo}</p>}
</form>
);
}useFormStatus — estado del formulario padre
useFormStatus (del paquete react-dom) permite que un componente hijo conozca el estado de envío del formulario más cercano:
import { useFormStatus } from 'react-dom';
function CampoConCarga({ label, name }: { label: string; name: string }) {
const { pending } = useFormStatus();
return (
<div>
<label htmlFor={name}>{label}</label>
<input
id={name}
name={name}
disabled={pending}
aria-busy={pending}
/>
</div>
);
}
function FormularioCompleto(): React.JSX.Element {
const [estado, accion] = useActionState(miAccion, {});
return (
<form action={accion}>
{/* useFormStatus funciona en componentes hijos del form */}
<CampoConCarga label="Email" name="email" />
<CampoConCarga label="Contraseña" name="password" />
<BotonSubmit />
</form>
);
}Accesibilidad en formularios
Los formularios accesibles deben cumplir con WCAG AA:
function CampoAccesible({
id, label, tipo = 'text', error, requerido = false,
}: {
id: string; label: string; tipo?: string; error?: string; requerido?: boolean;
}) {
const errorId = `${id}-error`;
return (
<div className="campo">
<label htmlFor={id}>
{label}
{requerido && <span aria-hidden="true"> *</span>}
{requerido && <span className="sr-only"> (requerido)</span>}
</label>
<input
id={id}
type={tipo}
required={requerido}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={error ? errorId : undefined}
aria-required={requerido}
/>
{error && (
<span id={errorId} role="alert" className="error">
{error}
</span>
)}
</div>
);
}Los atributos ARIA garantizan que los lectores de pantalla anuncien correctamente los errores y el estado de los campos, cumpliendo los criterios WCAG 3.3.1 (Error Identification) y 1.3.1 (Info and Relationships).
Inicia sesión para guardar tu progreso