
Gabriel Jiménez | Hace 6 días
En el artículo “Cómo hacer un formulario con React, desde 0”, explicamos qué es un formulario, cuando utilizarlos, las características principales y por último, diseñamos e implementamos un formulario. Sin embargo, siempre es bueno tener en nuestra caja de herramientas otras alternativas.
Es una librería para crear formularios en React de una forma más organizada. Esto lo logra al encapsular tres partes:
Podemos instalar Formik usando npm, yarn o incluir directamente el script.
Npm
npm install formik --save
Yarn
yarn add formik
Script
<script src=“https://unpkg.com/[email protected]/dist/formik.cjs.production.min.js”></script>
Para implementar Formik, utilizaremos un formulario React completamente en plano —es decir, sin ninguna librería externa— como ejemplo. Conforme avancemos, resaltaremos lo que Formik simplifica para nosotros.
Supongamos que un cliente que es panadero y necesita un formulario para poder registrar los pedidos de sus clientes. Los campos que necesitan son:
Conectar estado con la UI y lógica
Comencemos definiendo cada uno de los campos. Para esto, necesitamos un estado interno que llamaremos values, para guardar la data que ingrese el usuario.
1: import {useState} from "react"; 2: 3: const RegistroPedidos = () => { 4: const [values, setValues] = useState({ 5: nombre: "", 6: direccion: "", 7: tipoPastel: "", 8: observaciones: "", 9: fechaEntrega: "", 10: }) 11: 12: function handleChange({ target }) { 13: setValues({ ...values, [target.name]: target.value }) 14: } 15: 16: function handleSubmit() { 17: console.log("Enviando formulario", values) 18: } 19: 20: return( 21: <form onSubmit={handleSubmit}> 22: <input type='text' name='nombre' value={values.nombre} 23: placeholder='Nombre del cliente' 24: onChange={handleChange} 25: /> 26: 27: <input type='text' name='direccion' value={values.direccion} 28: placeholder='Dirección de entrega' 29: onChange={handleChange} 30: /> 31: 32: <select name='tipoPastel' value={values.tipoPastel} onChange={handleChange}> 33: <option>Seleccione una opción</option> 34: <option value='pasteldetresleches'>Pastel de tres lechas</option> 35: <option value='pasteldechocolate'>Pastel de chocolate</option> 36: </select> 37: 38: <textarea name='observaciones' value={values.observaciones} 39: placeholder='Observaciones' 40: onChange={handleChange} 41: /> 42: 43: <input type='date' name='fechaEntrega' value={values.fechaEntrega} 44: placeholder='Fecha de entrega' 45: onChange={handleChange} 46: /> 47: 48: <button type='submit'>Guardar</button> 49: </form> 50: ) 51: } 52: 53: export default RegistroPedidos;
El primer problema que encontramos, es que nosotros somos los responsables de:
A esto se le suele llamar “Wiring up state”.
NOTA: Dentro de React, hay una frase muy utilizada llamada “Wiring up state”. A grandes rasgos se entiende como conectar el estado con la UI y la lógica para todo reacción de forma coherente.
Veamos como Formik soluciona esto:
1: import { Formik, Form, Field } from "formik" 2: 3: const RegistroPedidos = () => { 4: return ( 5: <Formik 6: initialValues={{ 7: nombre: "", 8: direccion: "", 9: tipoPastel: "", 10: observaciones: "", 11: fechaEntrega: "", 12: }} 13: onSubmit={(values) => { 14: console.log("Enviando formulario", values) 15: }} 16: > 17: {() => ( 18: <Form> 19: <Field 20: type="text" 21: name="nombre" 22: placeholder="Nombre del cliente" 23: /> 24: 25: <Field 26: type="text" 27: name="direccion" 28: placeholder="Dirección de entrega" 29: /> 30: 31: <Field as="select" name="tipoPastel"> 32: <option value="">Seleccione una opción</option> 33: <option value="pasteldetresleches">Pastel de tres leches</option> 34: <option value="pasteldechocolate">Pastel de chocolate</option> 35: </Field> 36: 37: <Field 38: as="textarea" 39: name="observaciones" 40: placeholder="Observaciones" 41: /> 42: 43: <Field 44: type="date" 45: name="fechaEntrega" 46: placeholder="Fecha de entrega" 47: /> 48: 49: <button type="submit">Guardar</button> 50: </Form> 51: )} 52: </Formik> 53: ) 54: } 55: 56: export default RegistroPedidos
De esta forma, eliminamos el handleChange y el tener que asociarlo con cada uno de los campos.
Ahora, necesitamos validar que los campos sean obligatorios, excepto el campo observaciones.
Veamos como lograrlo sin usar Formik:
1: import { useState } from "react"; 2: 3: const RegistroPedidos = () => { 4: const [values, setValues] = useState({ 5: nombre: "", 6: direccion: "", 7: tipoPastel: "", 8: observaciones: "", 9: fechaEntrega: "", 10: }); 11: 12: const [errors, setErrors] = useState({}); 13: 14: function handleChange({ target }) { 15: setValues({ ...values, [target.name]: target.value }); 16: } 17: 18: function validate() { 19: const newErrors = {}; 20: 21: if (!values.nombre.trim()) { 22: newErrors.nombre = "El nombre es obligatorio"; 23: } 24: if (!values.direccion.trim()) { 25: newErrors.direccion = "La dirección es obligatoria"; 26: } 27: if (!values.tipoPastel.trim()) { 28: newErrors.tipoPastel = "Debe seleccionar un tipo de pastel"; 29: } 30: if (!values.fechaEntrega.trim()) { 31: newErrors.fechaEntrega = "La fecha de entrega es obligatoria"; 32: } 33: 34: return newErrors; 35: } 36: 37: function handleSubmit(e) { 38: e.preventDefault(); 39: 40: const validationErrors = validate(); 41: if (Object.keys(validationErrors).length > 0) { 42: setErrors(validationErrors); 43: return; 44: } 45: 46: console.log("Enviando formulario", values); 47: setErrors({}); 48: } 49: 50: return ( 51: <form onSubmit={handleSubmit}> 52: <div> 53: <input 54: type="text" 55: name="nombre" 56: value={values.nombre} 57: placeholder="Nombre del cliente" 58: onChange={handleChange} 59: /> 60: {errors.nombre && <div>{errors.nombre}</div>} 61: </div> 62: 63: <div> 64: <input 65: type="text" 66: name="direccion" 67: value={values.direccion} 68: placeholder="Dirección de entrega" 69: onChange={handleChange} 70: /> 71: {errors.direccion && <div>{errors.direccion}</div>} 72: </div> 73: 74: <div> 75: <select 76: name="tipoPastel" 77: value={values.tipoPastel} 78: onChange={handleChange} 79: > 80: <option value="">Seleccione una opción</option> 81: <option value="pasteldetresleches">Pastel de tres leches</option> 82: <option value="pasteldechocolate">Pastel de chocolate</option> 83: </select> 84: {errors.tipoPastel && <div>{errors.tipoPastel}</div>} 85: </div> 86: 87: <div> 88: <textarea 89: name="observaciones" 90: value={values.observaciones} 91: placeholder="Observaciones" 92: onChange={handleChange} 93: /> 94: </div> 95: 96: <div> 97: <input 98: type="date" 99: name="fechaEntrega" 100: value={values.fechaEntrega} 101: placeholder="Fecha de entrega" 102: onChange={handleChange} 103: /> 104: {errors.fechaEntrega && <div>{errors.fechaEntrega}</div>} 105: </div> 106: 107: <button type="submit">Guardar</button> 108: </form> 109: ); 110: }; 111: 112: export default RegistroPedidos;
El problema es que conectamos cada uno de los campos manualmente — como lo vimos anteriormente. Además, somos responsables por definir la ubicación donde se debe validar y esto puede ser un problema cuando se trabaja con grandes equipos, porque cada uno puede implementar la validación a su manera.
Veamos como Formik soluciona esto:
1: import { Formik, Form, Field, ErrorMessage } from "formik"; 2: 3: const RegistroPedidos = () => { 4: return ( 5: <Formik 6: initialValues={{ 7: nombre: "", 8: direccion: "", 9: tipoPastel: "", 10: observaciones: "", 11: fechaEntrega: "", 12: }} 13: validate={(values) => { 14: const errors = {}; 15: 16: if (!values.nombre.trim()) { 17: errors.nombre = "El nombre es obligatorio"; 18: } 19: if (!values.direccion.trim()) { 20: errors.direccion = "La dirección es obligatoria"; 21: } 22: if (!values.tipoPastel.trim()) { 23: errors.tipoPastel = "Debe seleccionar un tipo de pastel"; 24: } 25: if (!values.fechaEntrega.trim()) { 26: errors.fechaEntrega = "La fecha de entrega es obligatoria"; 27: } 28: 29: return errors; 30: }} 31: onSubmit={(values) => { 32: console.log("Enviando formulario", values); 33: }} 34: > 35: {() => ( 36: <Form> 37: <div> 38: <Field 39: type="text" 40: name="nombre" 41: placeholder="Nombre del cliente" 42: /> 43: <ErrorMessage name="nombre" component="div"/> 44: </div> 45: 46: <div> 47: <Field 48: type="text" 49: name="direccion" 50: placeholder="Dirección de entrega" 51: /> 52: <ErrorMessage name="direccion" component="div"/> 53: </div> 54: 55: <div> 56: <Field as="select" name="tipoPastel"> 57: <option value="">Seleccione una opción</option> 58: <option value="pasteldetresleches">Pastel de tres leches</option> 59: <option value="pasteldechocolate">Pastel de chocolate</option> 60: </Field> 61: <ErrorMessage name="tipoPastel" component="div"/> 62: </div> 63: 64: <div> 65: <Field 66: as="textarea" 67: name="observaciones" 68: placeholder="Observaciones" 69: /> 70: </div> 71: 72: <div> 73: <Field 74: type="date" 75: name="fechaEntrega" 76: placeholder="Fecha de entrega" 77: /> 78: <ErrorMessage name="fechaEntrega" component="div"/> 79: </div> 80: 81: <button type="submit">Guardar</button> 82: </Form> 83: )} 84: </Formik> 85: ); 86: }; 87: 88: export default RegistroPedidos;
De esta forma, cada que se haga clic en el botón guardar se ejecuta la función validate: si no hay ningún error se ejecuta onSubmit, en caso contrario, se muestra los errores.
NOTA: El componente <Formik> utiliza el patrón “render props”. Este patrón permite compartir código entre componentes utilizando una propiedad como función.
El manejo de envío de formularios se refiere a un proceso dentro de Formik para asegurarse que el envío de un formulario se esta haciendo correctamente. Cuando hacemos clic en el botón guardar, internamente Formik ejecuta las fases —pre-submit, validation y submission— que no son más que métodos.
Para finalizar, ¿qué sucede si no quiero usar los componentes que me proporciona Formik para crear mi formulario? ¿Hay alguna forma de evitarlo usarlos?
Cuando queremos un poco de más libertad para crear nuestros formularios y no estar atados a los componentes de Formik, el hook useFormik es la respuesta.
El hook useFormik tiene todo lo necesario como estados y funciones de ayuda para crear formularios desde 0.
Veamos el siguiente ejemplo:
1: import { useFormik } from "formik"; 2: 3: const RegistroPedidos = () => { 4: const formik = useFormik({ 5: initialValues: { 6: nombre: "", 7: direccion: "", 8: tipoPastel: "", 9: observaciones: "", 10: fechaEntrega: "", 11: }, 12: validate: (values) => { 13: const errors = {}; 14: 15: if (!values.nombre.trim()) { 16: errors.nombre = "El nombre es obligatorio"; 17: } 18: if (!values.direccion.trim()) { 19: errors.direccion = "La dirección es obligatoria"; 20: } 21: if (!values.tipoPastel.trim()) { 22: errors.tipoPastel = "Debe seleccionar un tipo de pastel"; 23: } 24: if (!values.fechaEntrega.trim()) { 25: errors.fechaEntrega = "La fecha de entrega es obligatoria"; 26: } 27: 28: return errors; 29: }, 30: onSubmit: async (values, { setSubmitting, resetForm }) => { 31: try { 32: console.log("Enviando formulario", values); 33: // Simulación de petición asíncrona 34: await new Promise((r) => setTimeout(r, 800)); 35: resetForm(); 36: } finally { 37: setSubmitting(false); 38: } 39: }, 40: }); 41: 42: return ( 43: <form onSubmit={formik.handleSubmit} noValidate> 44: <div> 45: <input 46: type="text" 47: name="nombre" 48: placeholder="Nombre del cliente" 49: onChange={formik.handleChange} 50: onBlur={formik.handleBlur} 51: value={formik.values.nombre} 52: /> 53: {formik.touched.nombre && formik.errors.nombre && ( 54: <div>{formik.errors.nombre}</div> 55: )} 56: </div> 57: 58: <div> 59: <input 60: type="text" 61: name="direccion" 62: placeholder="Dirección de entrega" 63: onChange={formik.handleChange} 64: onBlur={formik.handleBlur} 65: value={formik.values.direccion} 66: /> 67: {formik.touched.direccion && formik.errors.direccion && ( 68: <div>{formik.errors.direccion}</div> 69: )} 70: </div> 71: 72: <div> 73: <select 74: name="tipoPastel" 75: onChange={formik.handleChange} 76: onBlur={formik.handleBlur} 77: value={formik.values.tipoPastel} 78: > 79: <option value="">Seleccione una opción</option> 80: <option value="pasteldetresleches">Pastel de tres leches</option> 81: <option value="pasteldechocolate">Pastel de chocolate</option> 82: </select> 83: {formik.touched.tipoPastel && formik.errors.tipoPastel && ( 84: <div>{formik.errors.tipoPastel}</div> 85: )} 86: </div> 87: 88: <div> 89: <textarea 90: name="observaciones" 91: placeholder="Observaciones" 92: onChange={formik.handleChange} 93: onBlur={formik.handleBlur} 94: value={formik.values.observaciones} 95: /> 96: </div> 97: 98: <div> 99: <input 100: type="date" 101: name="fechaEntrega" 102: placeholder="Fecha de entrega" 103: onChange={formik.handleChange} 104: onBlur={formik.handleBlur} 105: value={formik.values.fechaEntrega} 106: /> 107: {formik.touched.fechaEntrega && formik.errors.fechaEntrega && ( 108: <div>{formik.errors.fechaEntrega}</div> 109: )} 110: </div> 111: 112: <button type="submit" disabled={formik.isSubmitting}> 113: {formik.isSubmitting ? "Enviando..." : "Guardar"} 114: </button> 115: </form> 116: ); 117: }; 118: 119: export default RegistroPedidos;
La creación se vuelve más verbosa y difícil de mantener.
Podríamos usar la siguiente estrategia para reducir un poco la duplicidad de código.
. . . 43: <form onSubmit={formik.handleSubmit} noValidate> 44: <div> 45: <input 46: type="text" 47: placeholder="Nombre del cliente" 48: {...formik.getFieldProps('nombre')} // Evitar duplicidad de código 49: /> 50: {formik.touched.nombre && formik.errors.nombre && ( 51: <div>{formik.errors.nombre}</div> 52: )} 53: </div> . . .
O si nos queremos poner un poco creativos, podemos crear nuestro propio componente de tipo formulario, como lo vimos en el artículo: Cómo hacer un formulario con React, desde 0
En lugar de permitir que cada quien decida su propio flujo para trabajar con formularios, Formik define su propio flujo de envío de formularios. Esto puede ser beneficio para grandes equipos de desarrollo, ya que al establecer reglas más puntuales para la creación de formularios, el código se vuelve más fácil de leer y mantener. Sin embargo, no todo es color de rosa, al agregar un capa más de abstracción en nuestra aplicación, puede volverse más difícil de debuguear y modificar. Por eso, antes de implementar una librería externa, es importante tener en cuenta la curva de aprendizaje, el soporte de la comunidad y las necesidades de cada proyecto.