En la práctica totalidad de las aplicaciones web que desarrollamos, nos encontramos con la necesidad de pedirle información a nuestras/os usuarias/os a través de formularios (desde un login simple hasta un complejo flujo de compra).

Aunque existen varias librerías para facilitarnos esta tarea, en este artículo vamos a dar a conocer la sencillez de React Hook Form y por qué la consideramos la mejor alternativa a día de hoy frente a sus competidoras.

¿Por qué React Hook Form?

Todos estaremos de acuerdo en que el desarrollo de un formulario en React no es una tarea compleja. Puede resultar monótono tener que ir añadiendo elemento a elemento, cuidando la apariencia de cada uno y guardando un sentido común. Pero no es realmente problemático.

El problema se nos presenta si tenemos en cuenta el rendimiento, sobre todo cuando tenemos muchos campos en nuestro formulario, con sus respectivas validaciones, y esto a su vez pueda estar replicado en varias partes de nuestra aplicación. Los elementos de un formulario en React se definen como componentes controlados ya que debemos ejercer un control sobre ellos para escuchar los eventos que puedan producirse y así actualizar el estado de nuestro componente con la nueva información. Como ya sabemos, toda acción de actualizar un componente, provocará un nuevo render de este, o lo que es lo mismo, volverá a pintarse en nuestro navegador.

React Hook Form nos ofrece la capacidad de desarrollar nuestros formularios de manera no controlada, independizando todo cambio que pueda producirse en cada uno de los elementos del formulario, evitando con ello renders innecesarios, haciendo uso de hooks y con una sencillez total.

Entre las características que la definen, cabe destacar:

Instalación

Podemos instalar la librería mediante los gestores de paquetes NPM o Yarn.

npm install react-hook-form --save

yarn add react-hook-form

Definiendo nuestro formulario

Para los siguientes ejemplos y comparativas, vamos a tomar como referencia un formulario sencillo en el que un usuario debe rellenar tres campos muy comunes en cualquier aplicación: su nombre, correo electrónico y número de teléfono móvil.

   <form>
     <label htmlFor="name">Nombre completo</label>
     <input id="name" type="text" />

     <label htmlFor="mail">Correo electrónico</label>
     <input id="mail" type="email" />

     <label htmlFor="phone">Teléfono móvil</label>
     <input id="phone" type="tel" placeholder="+34" />

     <input type="submit" />
   </form>

Como podéis ver, el formulario carece por completo de lógica para recoger la información de los distintos campos. Realizaremos la gestión primero desde una perspectiva controlada, sin librerías externas, para después hacerlo de una manera no-controlada delegando esta funcionalidad a nuestra nueva librería.

Gestión controlada de los elementos

En este primer ejemplo, empleando React por defecto, todo el protagonismo recae sobre el estado de nuestro componente. En él, almacenaremos en distintas propiedades los cambios que se produzcan en cada elemento del formulario:

 const [userInfo, setUserInfo] = useState({
   name: "",
   mail: "",
   phone: "",
 });

Dado que cada campo del formulario se interpreta como un nuevo elemento controlado, debemos recoger la información de todos ellos por medio de una función común que nos permita actualizar el estado interno de nuestro componente.

Esta acción la llevaremos a cabo por medio de la función handleChange que a su vez emplea setState, la cual es la única manera que nos proporciona React para actualizar el estado de un componente dada su inmutabilidad. Una vez recopilada la información en nuestro nuevo estado, asignaremos cada propiedad a su respectivo elemento por medio de su atributo value con el fin de reflejar en ellos los nuevos valores.

const handleChange = (event) => {
   const { name, value } = event.target;
   setUserInfo({ ...userInfo, [name]: value });
 };

 return (
   <form>
     <label htmlFor="name">Nombre completo</label>
     <input
       name="name"
       type="text"
       value={userInfo.name}
       onChange={handleChange}
     />

     <label htmlFor="mail">Correo electrónico</label>
     <input
       name="mail"
       type="email"
       value={userInfo.mail}
       onChange={handleChange}
     />

     <label htmlFor="phone">Teléfono móvil</label>
     <input
       name="phone"
       type="tel"
       placeholder="+34"
       value={userInfo.phone}
       onChange={handleChange}
     />

     <input type="submit" />
   </form>
 );

Hay que tener en consideración que con el ejemplo anterior estamos únicamente recogiendo información para mostrarla actualizada en cada campo, sin ningún tipo de validación previa del formato ni gestión de posibles errores que mostrar al usuario.

Incluir estas mejoras provocaría un incremento de propiedades en el estado actual o la necesidad de hacer nuevos estados dedicados específicamente a los posibles errores causados como resultado de nuestras validaciones previamente definidas.

En la gestión de manera controlada esto puede traer consigo problemas de rendimiento en aplicaciones con muchos componentes controlados debido a la constante actualización de los estados internos y al repintado o re-render del componente en pantalla. Esta problemática sería fácilmente solucionable si lográramos aislar los renders producidos por cada elemento, algo que veremos a continuación.

Puedes acceder al ejemplo completo en este enlace.

Gestión no-controlada o independiente

Aquí es donde entra en juego la librería que venimos a presentar, que nos va a permitir aislar todo render producido por cada uno de los elementos que componen nuestro formulario, minimizando casi al máximo su repintado y mejorando con ello la experiencia del usuario.

Para empezar a delegar la lógica de nuestro formulario, lo único que debemos importar en nuestro componente es el hook useForm que nos provee la librería.

import { useForm } from "react-hook-form";

A partir de este, obtendremos las funciones necesarias para simular la lógica que teníamos en el ejemplo anterior: recopilar la información de los elementos para posteriormente enviarla a un tercero.

const { register, handleSubmit } = useForm();

Para incorporar la función register en el formulario, bastará con añadirla como un nuevo atributo a la propia etiqueta HTML, por este motivo hemos resaltado en la introducción que la librería se caracteriza por respetar el estándar evitando tener que añadir nuevos componentes o wrappers a nuestras etiquetas ya existentes:

     <label htmlFor="name">Nombre completo</label>
     <input
       {...register("name")}
       name="name"
       type="text"
     />

     <label htmlFor="mail">Correo electrónico</label>
     <input
       {...register("mail")}
       name="mail"
       type="email"
     />

     <label htmlFor="phone">Teléfono móvil</label>
     <input
       {...register("phone")}
       name="phone"
       type="tel"
     />

El nombre introducido como parámetro en la función será como se denominen cada una de las propiedades incluidas en el objeto final listo para su envío.

Este objeto podremos obtenerlo haciendo uso de la función handleSubmit que incluiremos en el atributo onSubmit de nuestra etiqueta nativa <form> pasándole una función auxiliar que queramos, con el fin de que se ejecute al producirse el envío del formulario.

const onSubmit = (userInfo) => console.log(userInfo);
<form onSubmit={handleSubmit(onSubmit)}>

Con muy pocas líneas de código, y a falta de poder complicarlo mucho todavía, hemos dado vida a nuestro formulario delegando la recogida de información del usuario así como su envío a la par que mejoramos la performance, algo que demostraremos más adelante en un apartado dedicado a ello.

Puedes acceder al ejemplo completo en este enlace.

Incluyendo validaciones

Pasamos al momento de dar un poco de vidilla a nuestro formulario. Añadir validaciones en nuestro formulario, por muy básico que este sea, siempre debe ser un requisito indispensable de cara a asegurarnos que recogemos la información en un formato correcto para ser enviada donde queramos.

Esta tarea implica realizar comprobaciones individuales en cada uno de los campos para evitar que el usuario pueda insertar mal la información que esperamos de él, como por ejemplo: escribir texto en un campo dedicado al teléfono móvil, escribir un número de teléfono demasiado largo, escribir un correo electrónico sin respetar el formato, etc.

Dado que toda validación tendrá un resultado, bien sea satisfactorio o erróneo, esto nos obliga a realizar la gestión de errores: tenemos que poder informar al usuario de posibles mensajes de error sobre aquellos campos en los que se haya insertado incorrectamente la información solicitada. ¡Vamos a ello!

Para empezar a añadir nuestras validaciones en cada campo, basta con reutilizar la función register que ya incluimos en cada elemento y a la que podemos añadir cualquier validación del estándar HTML. Las validaciones que vamos a definir para nuestro ejemplo son las siguientes:

Para ello añadimos la propiedad required sobre un nuevo objeto a la función.

<input {...register("name", { required: true })} />
<input {...register("mail", { required: true })} />
<input {...register("phone", { required: true })} />

Con esto sería suficiente, pero como tenemos en mente realizar una gestión de errores es conveniente que dejemos un mensaje genérico que mostrar al usuario cuando pase por alto rellenar cualquiera de los campos obligatorios.

Definiremos un nuevo objeto que almacenará todos los posibles mensajes que mostremos al usuario en caso de error:

const messages = {
 req: "Este campo es obligatorio",
 name: "El formato introducido no es el correcto",
 mail: "Debes introducir una dirección correcta",
 phone: "Debes introducir un número correcto"
};

Así podremos incluir directamente el mensaje asociado a cada validación y lo usaremos seguidamente para mostrarlo por pantalla en el caso de producirse el error en la validación.

<input {...register("name", { required: messages.req })} />

No solo queremos asegurarnos de que el campo sea rellenado, sino también de que la información escrita sea la correcta. Por este motivo, definiremos otro nuevo objeto con las expresiones regulares que vayamos utilizando para asegurar el formato correcto.

Para el caso del nombre, añadiremos una nueva expresión que sólo permite incluir letras, con ello evitaremos que puedan introducirse números o caracteres especiales.

const patterns = { name: /^[A-Za-z]+$/i };

Podemos añadir nuestra nueva expresión regular directamente al objeto de la función register en una nueva propiedad denominada pattern, acompañado del mensaje que queramos mostrar al usuario cuando dicho patrón no se cumpla:

     <input
       {...register("name", {
         required: messages.required,
         pattern: {
           value: patterns.name,
           message: messages.name
         }
       })}

El campo del correo electrónico suele ser un clásico en casi cualquier formulario que nos encontremos. Por norma general, la validación siempre suele hacerse por medio de una expresión regular genérica que compruebe el formato habitual de un correo.

Actualizamos nuestro objeto de expresiones regulares con la nueva expresión y pasamos a añadirlo como nuevo patrón junto a su respectivo mensaje:

const patterns = {
     name: /^[A-Za-z]+$/i,
mail:/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
};
     <input
       name="mail"
       type="text"
       {...register("mail", {
         required: messages.required,
         pattern: {
           value: patterns.mail,
           message: messages.mail
         }
       })}

En nuestro último campo, necesitamos hacer dos validaciones: una, de formato, asegurando que no se escriban letras, ya que se trata de un número de teléfono; y otra, de longitud, ya que como conocemos a priori la longitud que tiene un número móvil en España, no nos conviene que nos escriban números con longitud inferior o mayor a 9 dígitos.

Para el primer caso procedemos igual que en los anteriores: definir la expresión regular en nuestro objeto y añadirla en nuestra etiqueta por medio de register. Para el segundo caso, la librería nos ofrece las reglas del estándar maxLength y minLength, que incluiremos a continuación como una nueva propiedad acompañado de su respectivo mensaje de error y el valor máximo o mínimo permitidos.

const patterns = {
 name: /^[A-Za-z]+$/i,
 mail: /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
 phone: /^[0-9]+$/i
};
     <input
       name="phone"
       type="tel"
       placeholder="+34"
       {...register("phone", {
         required: messages.required,
         minLength: {
           value: 9,
           message: messages.phone
         },
         maxLength: {
           value: 9,
           message: messages.phone
         },
         pattern: {
           value: patterns.phone,
           message: messages.phone
         }
       })}
     />

De esta manera tan simple y reutilizando la función que ya habíamos incorporado, hemos realizado todas nuestras validaciones sobre el formulario. Las validaciones podrían ser numerosas y más complejas en función del tipo de información que recopilemos del usuario.

En cualquier caso, esta es una tarea que siempre debemos intentar que esté recogida en el front y con ello, tratar de restar lógica y gestión de la información al back de nuestra aplicación, facilitando que sea éste quién reciba la información ya tratada y en un formato correcto para su gestión.

Tal y como hemos visto, hemos almacenado en dos objetos todas nuestras expresiones regulares y los mensajes de error en lugar de ir poniendo uno a uno en cada elemento. De esta manera somos mucho más organizados y nos resultará muy fácil modificar el formulario si en algún momento las propiedades pudieran cambiar o incrementar, del mismo modo que facilitamos la lectura a posibles desarrolladores que accedan a él.

Gestionando errores

Tenemos nuestras validaciones aplicadas en cada campo, pero de nada sirve validar lo que introduce el usuario si no le informamos debidamente en el caso de que algún dato lo haya introducido mal. Siempre que se produzca un error, debemos evitar que siga adelante con el formulario y sugerir las correcciones.

Para ello, la librería nos provee de un objeto errors que irá actualizándose con la información de aquellos campos que no han superado cualquiera de las validaciones que hayamos definido.

 const { formState: { errors } } = useForm();

De esta manera, siempre que el objeto errors contenga alguna propiedad, sabremos que algo ha salido mal y avisaremos de ello al usuario por medio del mensaje asociado a dicha validación. Las propiedades que forman este objeto serán cada uno de los campos que contenga nuestro formulario.

Supongamos un caso en el que el usuario se ha dejado sin rellenar el campo nombre y ha puesto un número de más en el campo teléfono. En este momento, el objeto errors almacenará un total de dos propiedades, una por cada validación incumplida, con la siguiente información: tipo de la validación, el elemento al que aplica y su respectivo mensaje asociado:

Dado que este objeto se actualiza dinámicamente, lo emplearemos para mostrar los mensajes de error comprobando previamente si dicha propiedad existe:

     <input
       name="mail"
       type="text"
       {...register("mail", {
         required: messages.required,
         pattern: {
           value: patterns.mail,
           message: messages.mail
         }
       })}
     />
     {errors.mail && {errors.mail.message}}

Por simplicidad, únicamente hemos añadido un mensaje por cada campo, pero gracias al campo type devuelto por el objeto errors, podríamos personalizar mucho más el feedback que le damos al usuario con el fin de ayudarle a entender mejor cuál ha sido el fallo cometido por su parte: error de formato, de longitud, de obligatoriedad, etc.

Puedes acceder al ejemplo completo en este enlace.

Comparando performance

Hemos destacado como una de las características más importantes de la librería su capacidad para mejorar el rendimiento de nuestros componentes debido a su forma de aislar los renders de un elemento frente a otro.

En nuestro ejemplo, esto se traduce en que si el usuario estuviera rellenando un campo cualquiera de nuestro formulario, esto no provocaría un repintado del formulario sobre nuestra aplicación debido a que estamos aplicando una gestión no-controlada en ese campo específico.

Para demostrar la comparativa de rendimiento de un formulario controlado frente a otro no-controlado, hemos incluido una variable contador en el cuerpo de nuestro formulario que incrementará su valor cada vez que se produzca un nuevo repintado o render.

Podemos comprobar que la diferencia es notable entre ambos casos. Vemos que para el caso del formulario no-controlado usando React Hook Form no se produce ningún render mientras el usuario edita cada uno de los campos, algo que ocurre de manera constante en el ejemplo contrario.

Los únicos renders para el caso no-controlado se producen al final del proceso, cuando el usuario clica en el botón para proceder a su envío. Esto es debido a que las validaciones se aplican por defecto cuando se envía el formulario, tras producirse el evento onSubmit, para finalmente informar al usuario de los posibles errores causados.

Dependiendo del caso concreto de nuestra aplicación, podríamos no querer que las validaciones esperaran hasta el último momento para aplicarse. Por este motivo la librería nos da la posibilidad de cambiar el modo en que se aplican sobre el formulario:

useForm({ mode: "onChange | onBlur | onTouched" });

Es interesante apreciar que aunque decidamos escoger el modo onChange, el cual no es recomendado por la librería por ser el que más penaliza el rendimiento, este sigue ofreciendo una mejora considerable con respecto a la gestión controlada:

Puedes acceder al ejemplo completo en este enlace.

Suscripción ante cambios

Existen situaciones en las que por algún motivo necesitamos escuchar los cambios que se produzcan en todo nuestro formulario o simplemente en algunos campos concretos del mismo. Esta necesidad nos puede surgir si elaboramos un formulario dinámico y quisiéramos mostrar u ocultar determinados elementos en base a la interacción del usuario.

Para llevar a cabo esta escucha o suscripción ante los cambios que puedan producirse en el formulario, sin provocar renders en el componente como nos ocurriría en un formulario con elementos controlados, la librería nos provee de la función watch a la que podremos indicarle el nombre de cualquier campo a ser observado o al conjunto completo de ellos.

const { watch } = useForm();

Un caso muy típico podría ser incluir un elemento checkbox en el formulario y que en base a su estado se inserten nuevos elementos dinámicamente. Para ello nos bastaría con observar el nombre asignado a nuestro elemento checkbox por medio de register:

const watchShowAddress = watch("showAddress");
<input type="checkbox" {...register("showAddress")} />

Y con la suscripción almacenada en una nueva variable, basarnos en ella para mostrar u ocultar el nuevo elemento:

     {watchShowAddress && (
       <>
         <label htmlFor="address">Dirección postal</label>
         <input name="address" type="text" {...register("address")} />
       </>
     )}

Por el contrario, un caso más avanzado podría ser querer realizar un tracking de toda la información que vaya escribiendo el usuario antes del envío del formulario con el fin de realizar métricas del comportamiento de los usuarios en nuestra aplicación.

Gracias a watch, esto es algo realmente fácil de implementar teniendo en cuenta que la recopilación de los datos será completamente invisible hacia el usuario y no provocará nuevos renders del componente que puedan perjudicar el rendimiento durante la interacción.

En este caso nos apoyaremos del hook useEffect que nos permitirá escuchar todo cambio que se produzca en nuestro formulario y a su vez cancelará la suscripción para evitar con ello el repintado del componente.

 useEffect(() => {
   const subscription = watch((data) => console.log(data));
   return () => subscription.unsubscribe();
 }, [watch]);

De esta manera tan sencilla, habremos realizado un tracking de toda interacción que tuviera el usuario en nuestro formulario para enviarlo a nuestra herramienta de métricas y así analizar el comportamiento que ha tenido el usuario desde el inicio hasta su envío. Todo ello sin afectar a la experiencia del usuario ni al rendimiento.

Puedes acceder al ejemplo completo en este enlace.

Conclusiones

Con la presentación de React Hook Form hemos podido ver una gran alternativa a la elaboración de formularios tradicionales si lo que buscamos es agilizar nuestros desarrollos a la par que simplificar nuestro código y ofrecer un buen rendimiento en nuestra aplicación.

Son muchos los factores que pueden llevar a nuestra aplicación a ofrecer un mal rendimiento durante su uso y es preciso tener en cuenta esta problemática en tareas comunes a cualquier desarrollo como puede ser la incorporación de un formulario. Reducir estas posibles causas de mal rendimiento en elementos tan frecuentes no sólo mejorará la experiencia del usuario sino que nos permitirá filtrar mejor en un futuro cuando surjan nuevos problemas de rendimiento.

Resulta interesante el hincapié de la librería en la experiencia del usuario no sólo por facilitar su integración en cuanto al código sino también por los recursos que ofrece en su página para construir visualmente el layout de tu formulario mediante drag & drop ó sus propias DevTools para hacer debug fácilmente desde la UI. Con ello, facilita enormemente esa primera toma de contacto del desarrollador.

Recomendaría el uso de esta librería en cualquier proyecto que busque o necesite mejorar determinados aspectos de su performance así como en proyectos personales o pruebas de concepto que necesiten un desarrollo ágil para publicar cuanto antes una primera versión que enseñar a interesados.

Debemos saber también que contamos con una amplia comunidad detrás de este proyecto, el cual recibe constantes actualizaciones y cuya última release fue subida 2 días antes de escribir este artículo. Sólo por este motivo, considero que ya merece la pena tenerla en cuenta.

Si te perdiste el anterior artículo sobre cómo agilizar tus desarrollos en React, puedes acceder a él desde aquí.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.