
Gabriel Jiménez | Hace alrededor de 1 mes
Existen muchas alternativas en el mercado para crear formularios con React: React Final Form, Formik, Material UI, React Bootstrap, React Hook Form. Este último, es una opción totalmente enfocada en la creación de formularios utilizando hooks. Los hooks nos permiten reutilizar código en diferentes partes de la aplicación. Dicho esto, veamos como React Hook Form reutiliza código usando hooks para crear formularios.
De forma general, React Hook Form es una librería para crear formularios mediante hooks.
Entre las características que los distinguen de otras alternativas son:
Para destacar las funcionalidades de React Hook Form, vamos a desarrollar un formulario únicamente utilizando React —no librerías externas—, y lo replicaremos a la par, usando React Hook Form.
Para este proyecto, usaremos la herramienta Create React App, ya que es un proyecto didáctico.
Para crearlo, ejecutemos los siguientes comandos:
npx create-react-app ejemplo-react-hook-form cd ejemplo-react-hook-form npm start
NOTA: Para proyectos más serie se recomienda usar herramientas como Vite.
Al ejecutar estos comandos, tendremos un proyecto configurado y listo para ejecutarse en el puerto localhost:3000
Instalar React Hook Form solo es necesario un comando.
Npm
npm install react-hook-form
Yarn
yarn install react-hook-form
Uno de nuestros clientes en el sector fintech, necesita desarrollar un formulario para registrar las devoluciones de sus productos.
Los requerimientos son los siguientes:
Comencemos con crear el formulario con React y sin apoyo de ninguna librería externa.
1: import React, {useState} from 'react' 2: 3: const DevolucionForm = () => { 4: const [values, setValues] = useState({ 5: producto: '', 6: monto: '', 7: observaciones: '' 8: }) 9: 10: function handleChange({ target }) { 11: const { value, name } = target; 12: setValues({ ...values, [name]: value }); 13: } 14: 15: function handleSubmit() { 16: console.log("Formulario enviado: " + values); 17: } 18: 19: return ( 20: <form onSubmit={handleSubmit}> 21: <select name='producto' onChange={handleChange}> 22: <option value="">Selecciona una opción</option> 23: <option value="producto a">Producto A</option> 24: <option value="producto b">Producto B</option> 25: </select> 26: <input type='number' name='monto' onChange={handleChange} value={values.monto} placeholder='Monto de devolucion' /> 27: <textarea name='observaciones' onChange={handleChange} value={values.observaciones} /> 28: </form> 29: ) 30: } 31: 32: export default DevolucionForm
Este tipo de formularios están bien cuando nuestros proyectos son pequeños, pero para proyectos grandes no son fáciles de mantener y escalar.
Aquí algunas de las razones:
Veamos como React Hook Form resuelve esto:
1: import React from "react"; 2: import { useForm } from "react-hook-form"; 3: 4: const DevolucionForm = () => { 5: const { register, handleSubmit } = useForm({ 6: defaultValues: { 7: producto: "", 8: monto: "", 9: observaciones: "", 10: }, 11: }); 12: 13: function onSubmit(data) { 14: console.log("Formulario enviado:", data); 15: } 16: 17: return ( 18: <form onSubmit={handleSubmit(onSubmit)}> 19: <select {...register("producto")}> 20: <option value="">Selecciona una opción</option> 21: <option value="producto a">Producto A</option> 22: <option value="producto b">Producto B</option> 23: </select> 24: 25: <input 26: type="number" 27: placeholder="Monto de devolución" 28: {...register("monto")} 29: /> 30: 31: <textarea 32: placeholder="Observaciones" 33: {...register("observaciones")} 34: /> 35: 36: <button type="submit">Enviar</button> 37: </form> 38: ); 39: }; 40: 41: export default DevolucionForm;
Más limpio. ¿Cierto?
Destaquemos varios puntos importantes:
Las validaciones para este ejercicio es que todos los campos sean obligatorios, excepto el campo observaciones.
Sin librería externa:
1: import React, { useState } from 'react' 2: 3: const DevolucionForm = () => { 4: const [values, setValues] = useState({ 5: producto: '', 6: monto: '', 7: observaciones: '' 8: }) 9: 10: const [errors, setErrors] = useState([]) 11: 12: function handleChange({ target }) { 13: const { value, name } = target 14: setValues({ ...values, [name]: value }) 15: } 16: 17: function handleSubmit(e) { 18: e.preventDefault() 19: const newErrors = [] 20: 21: if (!values.producto) { 22: newErrors.push("El campo 'Producto' es obligatorio") 23: } 24: 25: if (!values.monto) { 26: newErrors.push("El campo 'Monto' es obligatorio") 27: } 28: 29: setErrors(newErrors) 30: 31: if (newErrors.length === 0) { 32: console.log("Formulario enviado:", values) 33: } 34: } 35: 36: return ( 37: <form onSubmit={handleSubmit}> 38: {errors.length > 0 && ( 39: <div> 40: {errors.map((error, index) => ( 41: <div key={index}>{error}</div> 42: ))} 43: </div> 44: )} 45: 46: <select name='producto' value={values.producto} onChange={handleChange}> 47: <option value="">Selecciona una opción</option> 48: <option value="producto a">Producto A</option> 49: <option value="producto b">Producto B</option> 50: </select> 51: 52: <input 53: type='number' 54: name='monto' 55: value={values.monto} 56: placeholder='Monto de devolución' 57: onChange={handleChange} 58: /> 59: 60: <textarea 61: name='observaciones' 62: value={values.observaciones} 63: placeholder="Observaciones" 64: onChange={handleChange} 65: /> 66: 67: <button type="submit">Enviar</button> 68: </form> 69: ) 70: } 71: 72: export default DevolucionForm
Hay varias cosas que resaltar en este ejemplo:
Veamos la propuesta de React Hook Form.
React Hook Form:
1: import React from "react"; 2: import { useForm } from "react-hook-form"; 3: 4: const DevolucionForm = () => { 5: const { 6: register, 7: handleSubmit, 8: formState: { errors }, 9: } = useForm({ 10: defaultValues: { 11: producto: "", 12: monto: "", 13: observaciones: "", 14: }, 15: }); 16: 17: function onSubmit(data) { 18: console.log("Formulario enviado:", data); 19: } 20: 21: return ( 22: <form onSubmit={handleSubmit(onSubmit)}> 23: {Object.keys(errors).length > 0 && ( 24: <div> 25: {errors.producto && <div>El campo 'Producto' es obligatorio</div>} 26: {errors.monto && <div>El campo 'Monto' es obligatorio</div>} 27: </div> 28: )} 29: 30: <select 31: {...register("producto", { required: true })} 32: > 33: <option value="">Selecciona una opción</option> 34: <option value="producto a">Producto A</option> 35: <option value="producto b">Producto B</option> 36: </select> 37: 38: <input 39: type="number" 40: placeholder="Monto de devolución" 41: {...register("monto", { required: true })} 42: /> 43: 44: <textarea 45: placeholder="Observaciones" 46: {...register("observaciones")} 47: /> 48: 49: <button type="submit">Enviar</button> 50: </form> 51: ); 52: }; 53: 54: export default DevolucionForm;
Analicemos
NOTA: Entre las características principales de React Hook Form es utilizar los estándares HTML de validaciones. Así se evita reinventar la rueda. Al igual puede implementar validaciones más personalizadas.
Cabe mencionar que, los errores se muestran una vez que se envíe el formulario. Pero… si quiero ver la validación antes de enviarlo, ¿es posible? Si es posible, hablaremos de ello en otro artículo.
Una buena práctica en el desarrollo de software es realizar pruebas en nuestros desarrollos. Sin embargo, no todos lo hacen, en muchos casos debido a desconocimiento, tiempos de entrega muy ajustados, o simplemente porque se cree que, las pruebas son desarrolladas por el equipo de QA, pero esto no es así. El desarrollo de pruebas en el frontend es tan importante como en el backend, porque permite que, nuestros desarrollos sean más escalables y fáciles de mantener a largo paso.
Antes de saltar a implementar es importante conocer dos herramientas: Jest y React Testing Library.
De forma muy general, Jest es framework de tipo testing. se enfoca en configurar y proveer todas las herramientas necesarias para escribir y ejecutar nuestras pruebas.
React Testing Library es una librería para montar nuestros componentes y poder interactuar con ellos, de la misma forma como un usuario final.
Para instalar React Testing Library, basta con ejecutar el comando:
npm install --save-dev @testing-library/react @testing-library/dom
Creación de pruebas
Lo primero que debemos hacer al probar nuestros componentes es crear un archivo de pruebas. Generalmente, se usa el mismo nombre del componente que vamos a probar, en nuestro caso se llamará: DevolucionForm.test.jsx.
En el archivo van a vivir las pruebas, para nuestro caso vamos a tener dos:
Estas dos pruebas, se encargan de validar los requerimientos principales de nuestros ejemplo. Dicho esto, veamos el código.
DevolucionForm.test.js 1: describe('DevolucionForm', () => { 2: it('devolver un producto correctamente', () => { 3: }) 4: 5: it('si los campos obligatorios no son seleccionados, mostrar mensaje de error', () => { 6: }) 7: })
NOTA: Usamos las palabras reservadas “describe” y “it” para agrupar y definir cada una de las pruebas, respectivamente.
Ya que tenemos nuestras pruebas, ataquemos la primera.
Devolver un producto correctamente
Hablemos en términos no técnicos para entender más sobre las pruebas. Básicamente, lo que debemos hacer es, poder renderizar nuestro componente y comprobar que la información ingresada por el usuario se envíe correctamente al backend para decir que nuestra prueba ha pasado.
Veamos como luce el código:
. . . 5: it('devolver un producto correctamente', async () => { 6: const props = { 7: devolverProductoApi: jest.fn() 8: } 9: 10: render(<DevolucionForm {...props} />); 11: 12: fireEvent.change(screen.getByTestId('producto'), { target: { value: 'producto a' } }); 13: fireEvent.change(screen.getByTestId('monto'), { target: { value: '1000' } }); 14: fireEvent.change(screen.getByTestId('observaciones'), { target: { value: 'Producto malo' } }); 15: 16: fireEvent.submit(screen.getByText('Enviar')); 17: 18: await waitFor(() => { 19: expect(props.devolverProductoApi).toHaveBeenCalledWith({ 20: producto: 'producto a', 21: monto: '1000', 22: observaciones: 'Producto malo' 23: }) 24: }) 25: }) . . .
Analicemos
Línea 6-8
Crea el prop devolverProductoApi para simular la llamada al backend.
Línea 10
Renderizamos el componente devolucionForm usando la función render de React Testing Library.
Línea 12-14
Escribimos en los campos producto, monto y observaciones usando las funciones fireEvent y screen de la librería React Testing Library.
Línea 16
Hacemos clic en el botón enviar del formulario.
Línea 18-24
Comprobamos que los campos producto, monto y observaciones se hayan enviado correctamente al backend.
Antes de ejecutar la prueba debemos hacer un par de modificaciones en el código productivo:
. . . 4: const DevolucionForm = ({ devolverProductoApi }) => { . . . 17: async function onSubmit(data) { 18: await devolverProductoApi(data) 19: } 20: 21: return ( . . . 29: 30: <select {...register("producto", { required: true })} data-testid='producto'> . . . 34: </select> 35: 36: <input . . . 39: data-testid='monto' 40: {...register("monto", { required: true })} 41: /> 42: 43: <textarea . . . 45: data-testid='observaciones' 46: {...register("observaciones")} 47: /> . . .
Para ejecutar la prueba, usemos el siguiente comando:
npm test --t DevolucionForm
Ejecución de la pruebas
PASS src/DevolucionForm.test.js DevolucionForm ✓ devolver un producto correctamente (31 ms) ✓ si los campos obligatorios no son seleccionados, mostrar mensaje de error Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 0.54 s, estimated 1 s Ran all test suites matching /DevolucionForm/i. . . .
Perfecto, nuestra prueba “devolver un producto correctamente” ha pasado. Sin embargo, la otra también. Muy raro, ¿cierto? Esto es porque la otra prueba no estamos validando nada — por el momento.
Continuemos con la siguiente prueba.
Si los campos obligatorios no son seleccionados, mostrar mensaje de error
Para esta prueba, hay que comprobar que los mensaje de error cuando no seleccionamos los campos producto y monto se le estén mostrando al usuario.
Veamos el código:
. . . 27: it('si los campos obligatorios no son seleccionados, mostrar mensaje de error', async () => { 28: render(<DevolucionForm />); 29: 30: fireEvent.submit(screen.getByText('Enviar')); 31: 32: await waitFor(() => { 33: expect(document.body.textContent).toEqual(expect.stringContaining("El campo 'Producto' es obligatorio")); 34: expect(document.body.textContent).toEqual(expect.stringContaining("El campo 'Monto' es obligatorio")); 35: }) 36: }) . . .
Analicemos
Línea 28
Montamos el componente.
Línea 30
Hacemos clic en el botón enviar.
Línea 32-35
Comprobamos que las validaciones se le estén mostrando al usuario final.
Listo, hemos cubierto nuestros dos casos de prueba.
Al principio puede parecer bastante descabellado escribir más código, pero en aplicaciones grandes créeme que valdrá la pena.
Si quieres aprender más sobre testing en React, te recomiendo visitar mi página Testing en React
Crear formularios usando React Hook Form es bastante sencillo. Solo debemos usar el hook useForm, iniciar los valores, registrar cada uno de los campos usando el objeto register y pasar una función para invocarse cuando termine de ejecutar el flujo de React Hook Form.
No obstante, es importante identificar cuando utilizar una librería externa para crear nuestros formularios — no en todos los casos aplica. Para identificarlo, hay que tener en cuenta la curva de aprendizaje, el apoyo de la comunidad y los requerimientos del proyecto.
Si quieres aprender a crear formularios más complejos, y no solo eso, sino desde un enfoque de pruebas. Te invito a leer mi libro: Testing en React: Guía práctica con Jest y React Testing Library.