En pocas palabras
Biblioteca para gestionar estado, validación y envío de formularios en React, reduciendo renderizados innecesarios mediante suscripciones y campos no controlados por defecto.
Qué problema resuelve
Exceso de código para manejar campos y errores, y problemas de rendimiento en formularios grandes o dinámicos.
Cuándo utilizarlo
Altas de usuarios, formularios administrativos y flujos de varios pasos. Ejemplo: validar una solicitud con Zod y mostrar errores por campo sin actualizar todo el formulario.
De la idea a la práctica
Funcionamiento, ejemplos y decisiones para pasar al siguiente nivel.
Cómo funciona
Los campos se registran con register; las suscripciones actualizan las partes que necesitan cambios. Los resolvers conectan esquemas como Zod o Yup. Controller adapta componentes controlados de bibliotecas de interfaz.
Ejemplo: un campo requerido
En un proyecto React con react-hook-form instalado:
import { useForm } from 'react-hook-form';
export function Formulario() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<{ nombre: string }>();
return (
<form onSubmit={handleSubmit((datos) => console.log(datos))}>
<label htmlFor="nombre">Nombre</label>
<input
id="nombre"
{...register('nombre', {
required: 'Escribe tu nombre',
})}
aria-invalid={Boolean(errors.nombre)}
aria-describedby={errors.nombre ? 'error-nombre' : undefined}
/>
{errors.nombre && <p id="error-nombre">{errors.nombre.message}</p>}
<button type="submit">Guardar</button>
</form>
);
}
Validación con Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const esquema = z.object({
documento: z.string().regex(/^\d{6,10}$/, 'Documento de 6 a 10 dígitos'),
monto: z.coerce
.number()
.min(100_000, 'Mínimo 100 000')
.max(50_000_000, 'Máximo 50 000 000'),
plazo: z.coerce.number().int().min(3).max(60),
aceptaTerminos: z.literal(true, { message: 'Debes aceptar los términos' }),
});
type Solicitud = z.infer<typeof esquema>;
export function FormularioSolicitud() {
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<Solicitud>({ resolver: zodResolver(esquema), mode: 'onBlur' });
const enviar = async (datos: Solicitud) => {
const res = await fetch('/api/solicitudes', {
method: 'POST',
body: JSON.stringify(datos),
});
if (res.status === 409) {
setError('documento', {
message: 'Ya existe una solicitud en curso para este documento',
});
return;
}
if (!res.ok)
setError('root.servidor', {
message: 'No pudimos enviar la solicitud. Inténtalo de nuevo.',
});
};
return (
<form onSubmit={handleSubmit(enviar)} noValidate>
{/* campos con register('documento'), register('monto')… */}
{errors.root?.servidor && (
<p role="alert">{errors.root.servidor.message}</p>
)}
<button disabled={isSubmitting}>
{isSubmitting ? 'Enviando…' : 'Enviar'}
</button>
</form>
);
}
El mismo esquema de Zod puede reutilizarse en el servidor, de modo que cliente y backend validan con las mismas reglas. setError muestra en el campo los errores que solo el servidor puede detectar.
Listas dinámicas con useFieldArray
const { control, register } = useForm<{
referencias: { nombre: string; telefono: string }[];
}>({
defaultValues: { referencias: [{ nombre: '', telefono: '' }] },
});
const { fields, append, remove } = useFieldArray({
control,
name: 'referencias',
});
{
fields.map((campo, i) => (
<fieldset key={campo.id}>
<legend>Referencia {i + 1}</legend>
<input {...register(`referencias.${i}.nombre`)} />
<input {...register(`referencias.${i}.telefono`)} />
<button type="button" onClick={() => remove(i)}>
Quitar
</button>
</fieldset>
));
}
<button type="button" onClick={() => append({ nombre: '', telefono: '' })}>
Añadir referencia
</button>;
Usa campo.id como key, nunca el índice, para que React no confunda filas al eliminar.
Componentes controlados de librerías de UI
<Controller
name="fechaNacimiento"
control={control}
render={({ field, fieldState }) => (
<DatePicker
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
error={fieldState.error?.message}
/>
)}
/>
Modos de validación
mode |
Cuándo valida | Experiencia |
|---|---|---|
onSubmit |
Al enviar (por defecto) | Sin errores prematuros |
onBlur |
Al salir de cada campo | Buen equilibrio para formularios largos |
onChange |
En cada pulsación | Feedback inmediato, puede resultar molesto |
onTouched |
Tras el primer blur, luego en cada cambio | Combina ambos |
Formularios largos por pasos
Para solicitudes de crédito en varios pasos, un único useForm con trigger(['campo1', 'campo2']) permite validar solo los campos del paso actual antes de avanzar, y conservar todos los datos si la persona vuelve atrás. Guarda borradores en el servidor si el formulario es largo, no solo en el navegador. Revisa los formularios con axe-core y con teclado.
Criterios de implementación
Asocia mensajes y etiquetas a sus campos y facilita el foco del primer error. Conserva los datos cuando falle el envío. La validación del cliente mejora la interacción; el servidor debe validar de nuevo y aplicar permisos.
Continúa en las fuentes
Documentación oficial para verificar detalles y explorar más.