Optimiza tus formularios en React: más allá de useState y los básicos de react-hook-form

Close-up of JavaScript code on a laptop screen, showcasing programming in progress.
Foto de Markus Winkler en Pexels

En los últimos años react-hook-form se ha convertido en la opción preferida para gestionar formularios en React. Si todavía usas useState para cada campo, perderás tiempo y rendimiento. En este artículo vamos a profundizar en técnicas intermedias que suelen pasar desapercibidas: validaciones asíncronas, integración con librerías UI y la creación de hooks reutilizables.

¿Por qué dejar useState?

Controlar cada input con useState funciona, pero cada render implica volver a crear los handlers y a actualizar el árbol de componentes. react-hook-form registra los ref de los elementos y mantiene el estado fuera del árbol, lo que reduce renders innecesarios y mejora la experiencia del usuario en formularios extensos.

Primeros pasos con react-hook-form

Instala la librería y crea un formulario simple. Observa cómo el registro de campos es mucho más conciso que con useState:

import React from 'react';
import { useForm } from 'react-hook-form';

export default function SimpleForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email', { required: 'El email es obligatorio' })} />
      {errors.email && <span>{errors.email.message}</span>}
      <button type="submit">Enviar</button>
    </form>
  );
}

Con solo una línea ({...register('email')}) ya tienes el binding, la gestión de errores y la revalidación automática.

Validaciones sincrónicas y asíncronas

Para la mayoría de los campos basta con reglas simples, pero a veces necesitamos comprobar datos en el servidor (por ejemplo, si un nombre de usuario está disponible). react-hook-form permite usar validadores asíncronos mediante la opción validate:

const checkUsername = async username => {
  const res = await fetch(/api/users/check?username=${username});
  const { available } = await res.json();
  return available || 'Ese nombre ya está en uso';
};

// Dentro del registro del input
{...register('username', { required: true, validate: checkUsername })}

El formulario mostrará el mensaje de error tan pronto como la promesa se resuelva, sin bloquear el resto de la UI.

Integrando UI de terceros

Componentes como Material‑UI o Ant Design usan sus propios onChange y a veces no exponen directamente la referencia del input. En esos casos podemos usar el método Controller que provee react-hook-form para conectar cualquier componente controlado.

import { Controller, useForm } from 'react-hook-form';
import { TextField } from '@mui/material';

export default function MuiForm() {
  const { control, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="firstName"
        control={control}
        defaultValue=""
        render={({ field }) => (
          <TextField {...field} label="Nombre" variant="outlined" />
        )}
      />
      <button type="submit">Enviar</button>
    </form>
  );
}

El Controller se encarga de pasar value y onChange al componente externo, manteniendo la lógica de validación intacta.

Creando un hook reutilizable

Si trabajas con varios formularios que comparten lógica (por ejemplo, cargar opciones desde una API o resetear campos después de enviar), puedes encapsular esa lógica en un hook propio:

import { useForm } from 'react-hook-form';
import { useEffect } from 'react';

export function useUserForm(initialValues = {}) {
  const { register, handleSubmit, reset, setError, formState } = useForm({ defaultValues: initialValues });

  // Cargar datos de usuario al montar
  useEffect(() => {
    async function fetchUser() {
      const res = await fetch('/api/user/me');
      const data = await res.json();
      reset(data);
    }
    fetchUser();
  }, [reset]);

  const onSubmit = async data => {
    try {
      const res = await fetch('/api/user', { method: 'PUT', body: JSON.stringify(data) });
      if (!res.ok) throw new Error('Error al actualizar');
    } catch (e) {
      setError('api', { type: 'manual', message: e.message });
    }
  };

  return { register, handleSubmit: handleSubmit(onSubmit), formState, errors: formState.errors };
}

Con este hook, cualquier componente que necesite el formulario de usuario solo tiene que llamarlo y renderizar los campos habituales.

Conclusión

Pasar de useState a react-hook-form es sólo el primer paso. Aprovechar las validaciones asíncronas, conectar UI de terceros con Controller y crear hooks reutilizables te permitirá escalar tus formularios sin perder rendimiento ni claridad. La próxima vez que tengas que crear un formulario complejo, prueba estas técnicas y verás cómo tu código se vuelve más limpio y mantenible.