
Gabriel Jiménez | Hace 6 días
Construir un formulario no es tal difícil al principio. El problema viene cuando empezamos a tener más de uno — cada uno con su propia lógica de manejo de estado, validaciones y gestión de errores — se vuelven difíciles mantener y debuguear. Cuando esto sucede, es bueno voltear a ver herramientas de terceros y ver si se ajustan a nuestras necesidades. Hoy toca ver una de esas herramientas, React Final Form.
Es una librería para crear formularios en React de manera sencilla. Esta construido usando como base Final Form.
NOTA: Final Form es un framework encargado de gestionar todo el estado de un formulario, basado en suscripciones que utiliza el patrón Observer, por lo que solo los componentes, que necesitan actualización se vuelven a renderizar a medida que cambia el estado del formulario.
Las características principales que lo hacen diferente con respecto a otras soluciones son:
Instalar React Final Form es bastante sencillo, podemos usar npm o yarn.
Npm
npm install --save final-form react-final-form
Yarn
yarn add final-form react-final-form
Para entender mejor como funciona React Final Form, construiremos un formulario únicamente utilizando React y veremos paso a paso como sustituirlo con React Final Form.
Problematica
Nuestro cliente es una empresa dedicada a la venta de flores. Su negocio a crecido con el tiempo y necesita una sección donde sus clientes realicen pedidos.
La sección necesita de un formulario con los siguientes campos:
**Todos los campos son obligatorios, excepto el campo observaciones.**
Comencemos construyendo el formulario sin ninguna librería externa, únicamente React.
1: import {useState} from "react"; 2: 3: const PedidoFlores = () => { 4: const [values, setValues] = useState({ 5: nombre: "", 6: direccion: "", 7: productos: null, 8: fechaEntrega: null, 9: observaciones: null, 10: }) 11: 12: function handleSubmit() { 13: } 14: 15: function handleChange({ target }) { 16: const { value, name } = target; 17: setValues({ ...values, [name]: value }); 18: } 19: 20: return ( 21: <form onSubmit={handleSubmit}> 22: <input type="text" name='nombre' value={values.nombre} placeholder="Nombre del cliente" 23: onChange={handleChange} 24: /> 25: 26: <input type="text" name='direccion' value={values.direccion} placeholder="Dirección de entrega" 27: onChange={handleChange} 28: /> 29: 30: <input type="text" name='productos' value={values.productos} placeholder="Productos solicitados" 31: onChange={handleChange} 32: /> 33: 34: <input type="date" name='fechaEntrega' value={values.fechaEntrega} placeholder="Fecha de entrega" 35: onChange={handleChange} 36: /> 37: 38: <textarea name='observaciones' value={values.observaciones} placeholder="Observaciones" 39: onChange={handleChange} 40: /> 41: </form> 42: ) 43: } 44: 45: export default PedidoFlores;
Formularios como estos funcionan perfectamente en aplicaciones pequeñas donde tienes pocos formularios, pero en aplicaciones con más de 10 formularios —por decir un número— se vuelve difícil de mantener, aquí algunas de las razones:
Sincronización de UI. En aplicaciones grandes donde necesitamos mantener los estilos de la marca, es importante que la UI este alineada a los colores, tipos de letras, placeholders, etc. Al darle la libertad a cada desarrollador sobre decidir los estilos de cada uno, tendremos diferentes formularios en cada sección.
Gestión del estado. Nosotros somos responsables de conectar manualmente el estado values con cada campo. Además, de asociar el handleChange a cada uno de los campos. A esto se le llama “Wiring up state”.
NOTA: En el mundillo de React, la frase “Wiring up state”, se entiende como, conectar el estado con la UI y la lógica para todo reacción de forma coherente.
Veamos como React Final Form resuelve esto:
1: import { Form, Field } from "react-final-form"; 2: 3: const PedidoFloresReactFinalForm = () => { 4: const initialValues = { 5: nombre: "", 6: direccion: "", 7: productos: "", 8: fechaEntrega: "", 9: observaciones: "", 10: }; 11: 12: const onSubmit = (values) => { 13: console.log("Datos del pedido:", values); 14: }; 15: 16: return ( 17: <Form 18: initialValues={initialValues} 19: onSubmit={onSubmit} 20: render={({ handleSubmit }) => ( 21: <form onSubmit={handleSubmit}> 22: <Field 23: name="nombre" 24: component="input" 25: type="text" 26: placeholder="Nombre del cliente" 27: /> 28: 29: <Field 30: name="direccion" 31: component="input" 32: type="text" 33: placeholder="Dirección de entrega" 34: /> 35: 36: <Field 37: name="productos" 38: component="input" 39: type="text" 40: placeholder="Productos solicitados" 41: /> 42: 43: <Field 44: name="fechaEntrega" 45: component="input" 46: type="date" 47: placeholder="Fecha de entrega" 48: /> 49: 50: <Field 51: name="observaciones" 52: component="textarea" 53: placeholder="Observaciones" 54: /> 55: 56: <button type="submit">Enviar</button> 57: </form> 58: )} 59: /> 60: ); 61: }; 62: 63: export default PedidoFloresReactFinalForm;
Bastante más limpio y sencillo. Ahora todos nuestros formularios deberían seguir estos tres pasos:
Para finalizar con nuestro ejercicio, resolvamos el tema de las validaciones.
Nuestro formulario solo permite enviarlo cuando todos los campos están llenos, excepto el campo observaciones.
Primero veamos como resolverlo sin utilizar ninguna librería externa:
1: import { useState } from "react"; 2: 3: const PedidoFlores = () => { 4: const [values, setValues] = useState({ 5: nombre: "", 6: direccion: "", 7: productos: "", 8: fechaEntrega: "", 9: observaciones: "", 10: }); 11: 12: const [errors, setErrors] = useState({}); 13: 14: function validate() { 15: const newErrors = {}; 16: if (!values.nombre) newErrors.nombre = "Requerido"; 17: if (!values.direccion) newErrors.direccion = "Requerido"; 18: if (!values.productos) newErrors.productos = "Requerido"; 19: if (!values.fechaEntrega) newErrors.fechaEntrega = "Requerido"; 20: return newErrors; 21: } 22: 23: function handleSubmit(e) { 24: e.preventDefault(); 25: const validationErrors = validate(); 26: if (Object.keys(validationErrors).length > 0) { 27: setErrors(validationErrors); 28: return; 29: } 30: setErrors({}); 31: console.log("Datos del pedido:", values); 32: } 33: 34: function handleChange({ target }) { 35: const { name, value } = target; 36: setValues({ ...values, [name]: value }); 37: } 38: 39: return ( 40: <form onSubmit={handleSubmit}> 41: <div> 42: <input 43: type="text" 44: name="nombre" 45: value={values.nombre} 46: placeholder="Nombre del cliente" 47: onChange={handleChange} 48: /> 49: {errors.nombre && <span>{errors.nombre}</span>} 50: </div> 51: 52: <div> 53: <input 54: type="text" 55: name="direccion" 56: value={values.direccion} 57: placeholder="Dirección de entrega" 58: onChange={handleChange} 59: /> 60: {errors.direccion && <span>{errors.direccion}</span>} 61: </div> 62: 63: <div> 64: <input 65: type="text" 66: name="productos" 67: value={values.productos} 68: placeholder="Productos solicitados" 69: onChange={handleChange} 70: /> 71: {errors.productos && <span>{errors.productos}</span>} 72: </div> 73: 74: <div> 75: <input 76: type="date" 77: name="fechaEntrega" 78: value={values.fechaEntrega} 79: placeholder="Fecha de entrega" 80: onChange={handleChange} 81: /> 82: {errors.fechaEntrega && <span>{errors.fechaEntrega}</span>} 83: </div> 84: 85: <div> 86: <textarea 87: name="observaciones" 88: value={values.observaciones} 89: placeholder="Observaciones" 90: onChange={handleChange} 91: /> 92: </div> 93: 94: <button type="submit">Enviar</button> 95: </form> 96: ); 97: }; 98: 99: export default PedidoFlores;
De igual manera, la gestión de validaciones y errores se puede volver difícil de mantener. Debido a que, cada formulario puede implementar las validaciones a su manera y renderizar los errores en diferentes ubicaciones.
React Final Form lo resuelve pasando el prop validate.
NOTA: Validate es una función, que se ejecuta justo después de que el usuario envía el formulario. Si una validación falla, se rompe el flujo y inmediatamente se muestra los errores. En caso contrario, se invoca la función onSubmit.
1: import { Form, Field } from "react-final-form"; 2: 3: const PedidoFlores = () => { 4: const initialValues = { 5: nombre: "", 6: direccion: "", 7: productos: "", 8: fechaEntrega: "", 9: observaciones: "", 10: }; 11: 12: // Validador general 13: const validate = (values) => { 14: const errors = {}; 15: if (!values.nombre) { 16: errors.nombre = "Requerido"; 17: } 18: if (!values.direccion) { 19: errors.direccion = "Requerido"; 20: } 21: if (!values.productos) { 22: errors.productos = "Requerido"; 23: } 24: if (!values.fechaEntrega) { 25: errors.fechaEntrega = "Requerido"; 26: } 27: return errors; 28: }; 29: 30: const onSubmit = (values) => { 31: console.log("Datos del pedido:", values); 32: }; 33: 34: return ( 35: <Form 36: initialValues={initialValues} 37: validate={validate} 38: onSubmit={onSubmit} 39: render={({ handleSubmit }) => ( 40: <form onSubmit={handleSubmit}> 41: <div> 42: <Field 43: name="nombre" 44: component="input" 45: type="text" 46: placeholder="Nombre del cliente" 47: /> 48: <Field name="nombre" subscription={{ error: true, touched: true }}> 49: {({ meta: { error, touched } }) => 50: touched && error ? <span>{error}</span> : null 51: } 52: </Field> 53: </div> 54: 55: <div> 56: <Field 57: name="direccion" 58: component="input" 59: type="text" 60: placeholder="Dirección de entrega" 61: /> 62: <Field name="direccion" subscription={{ error: true, touched: true }}> 63: {({ meta: { error, touched } }) => 64: touched && error ? <span>{error}</span> : null 65: } 66: </Field> 67: </div> 68: 69: <div> 70: <Field 71: name="productos" 72: component="input" 73: type="text" 74: placeholder="Productos solicitados" 75: /> 76: <Field name="productos" subscription={{ error: true, touched: true }}> 77: {({ meta: { error, touched } }) => 78: touched && error ? <span>{error}</span> : null 79: } 80: </Field> 81: </div> 82: 83: <div> 84: <Field 85: name="fechaEntrega" 86: component="input" 87: type="date" 88: placeholder="Fecha de entrega" 89: /> 90: <Field 91: name="fechaEntrega" 92: subscription={{ error: true, touched: true }} 93: > 94: {({ meta: { error, touched } }) => 95: touched && error ? <span>{error}</span> : null 96: } 97: </Field> 98: </div> 99: 100: <div> 101: <Field 102: name="observaciones" 103: component="textarea" 104: placeholder="Observaciones" 105: /> 106: </div> 107: 108: <button type="submit">Enviar</button> 109: </form> 110: )} 111: /> 112: ); 113: }; 114: 115: export default PedidoFlores;
Perfecto, ya sabemos lo básico para desarrollar formularios con React Final Form. Sin embargo, vayamos más allá.
¿Qué sucede si quiero personalizar algún Field en particular?
Personalización de Fields
Para personalizar un Field tenemos tres opciones:
Veamos un ejemplo de cada uno:
Prop render
<div> <Field name="fechaEntrega" render={({ input, meta }) => ( <div> <input {...input} type="date" placeholder="Fecha de entrega" /> {meta.touched && meta.error && ( <span>{meta.error}</span> )} </div> )} /> </div>
Prop component
// Componente personalizado const DateField = ({ input, meta, ...rest }) => ( <div> <input {...input} {...rest} /> {meta.touched && meta.error && <span>{meta.error}</span>} </div> ); // Uso en el Field <Field name="fechaEntrega" component={DateField} type="date" placeholder="Fecha de entrega" />
Función tener como children
<Field name="fechaEntrega"> {({ input, meta }) => ( <div> <input {...input} type="date" placeholder="Fecha de entrega" /> {meta.touched && meta.error && <span>{meta.error}</span>} </div> )} </Field>
¿Cuál tipo utilizar? Si tuviera que elegir uno, me iría por utilizar Prop component, ya que es menos verboso que los otros tipos. Pero cualquiera, cumple con la función de utilizar toda la funcionalidad de React Final Form.
React Final Form es una buena herramienta para simplificar el proceso de creación de formularios. Establece un flujo que todos puedan seguir y facilita la personalización para adaptarse a los requerimientos de cada proyecto. Sin embargo, antes de implementarlo, es importante considerar la curva de aprendizaje, el apoyo de la comunidad y los requerimientos del proyecto.
Si quieres aprender a resolver formularios más complejos, te envío a leer mi libro: Testing en React: Guía práctica con Jest y React Testing Library.