Crea formularios con React Final Form desde cero

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.

¿Qué es 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:


  • Modular. Se refiere a que cada pieza dentro de React Final Form hace una tarea específica.
  • Cero dependencias. Las únicas dependencias necesarias son React y Final Form.
  • Alto rendimiento. Permite renderizar ciertos campos, en lugar de todo el formulario.
  • Compatibilidad con hooks. Se mantiene sincronizado con las nuevas versiones de React.

¿Cómo instalar React Final Form?

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

¿Cómo implementar React Final Form en mi proyecto React?

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:


  • Nombre del cliente.
  • Dirección de entrega.
  • Productos solicitados.
  • Fecha de entrega.
  • Observaciones.

**Todos los campos son obligatorios, excepto el campo observaciones.**

Implementación

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:


  1. Definir los campos del formulario en la variable initialValues.
  2. Utilizar los componentes de React Final Form: Form y Field, con la finalidad de mantener coherencia entre cada formulario.
  3. Asociar una función para que se invoque una vez que el usuario envía el formulario.

Para finalizar con nuestro ejercicio, resolvamos el tema de las validaciones.




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:


  1. Usar el prop render.
  2. Usar el prop component.
  3. Utilizar una función render como children. 

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.


Conclusión

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.