Alejandría

Busca en Alejandría

Lenguajes y marcos

React Hook Form

Gestiona formularios React con validación y menos renderizados.

01

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.

02

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.