Formik en React: cómo crear tu primer formulario desde 0

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.


¿Qué es Formik?

Es una librería para crear formularios en React de una forma más organizada. Esto lo logra al encapsular tres partes:


  1. Obtención de valores dentro y fuera del estado del formulario. 

  1. Validaciones y mensajes de error.
  1. Manejo de envió de formularios.

Para entenderlo mejor, revisemos la figura 1.

Figura 1. Errores más comunes en los formularios. Cuando desarrollamos formularios sin librerías externas, es frecuente repetir lo siguiente: manejar manualmente el estado y su conexión con la UI, repetir la lógica de validaciones, desorganizar el código y complicar las pruebas.


NOTA: Siempre que usemos una librería externa es importante tener la curva de aprendizaje, si existe comunidad y el peso. Recuerda que entre más capas agreguemos a nuestra aplicación más difícil se vuelve mantener nuestra aplicación si no planificamos bien desde un inicio.

¿Cómo instalar Formik?

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>

¿Cómo implementar Formik en mi proyecto React?

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.


Problematica

Supongamos que un cliente que es panadero y necesita un formulario para poder registrar los pedidos de sus clientes. Los campos que necesitan son:


  1. Nombre del cliente.
  1. Dirección de entrega.
  1. Tipo de pastel.
  1. Observaciones.
  1. Fecha de entrega.

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:


  • Conectar manualmente el estado values con cada campo.
  • Asociar el handleChange a cada uno de los campos. 


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.


Validaciones y mensajes de error

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.


Manejo de envío de formularios

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.



Veamos la figura 2 para entenderlo mejor.

Figura 2. Fases de envío en Formik. La fases de envío pre-submit, validation y submission se ejecutan cada que el usuario dispara el método handleSubmit o submitForm.


De forma muy general, el flujo de envío de formulario es el siguiente:
  1. El usuario hace clic en el botón Guardar. Dispara handleSubmit / submitForm
  2. Comienza la fase de pre submit. Marcan todos los campos como “touched”, esto para que muestre inmediatamente los mensajes de error arrojados en la fase de validación.
  3. Ejecuta la fase de validación. Se ejecutan las validaciones.
    1. Si existen error aborta y se sale del flujo.
    2. En caso contrario, si no existen errores de validación, el flujo continua.
  4. Ejecuta la fase de onSubmit. Consiste en determinar si se están ejecutando peticiones síncronas o asíncronas.
    1. Si la lógica que ejecutamos dentro del onSubmit no resuelve alguna promesa, el isSubmitting se asigna false inmediatamente.
    2. En caso contrario, si se esta resolviendo una promesa, el isSubmitting es true hasta que la promesa termine. Esto es útil para desactivar botones, mostrar spinners, etc.

Para finalizar, ¿qué sucede si no quiero usar los componentes que me proporciona Formik para crear mi formulario? ¿Hay alguna forma de evitarlo usarlos?

useFormik hook

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


Conclusión

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.