En este tutorial vamos a integrar un formulario en React donde mostraremos mensajes de error cuando los campos no cumplan con las validaciones, un estado de loading cuando se haga submit y un mensaje de éxito cuando el formulario se envíe correctamente.

Para ello vamos a utilizar la librería Formik que nos permite manejar el estado de los formularios en React.

Vista previa formulario

Estilos de componentes

Para los estilos de los componentes vamos a utilizar el framework CSS Bulma. Para instalarlo vamos a utilizar el CDN de Bulma y lo agregamos al archivo index.html dentro de la etiqueta <head>.

public/index.html
<head>
  <!-- ... -->
  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css"
  />
</head>

Código base

Para iniciar creamos el componente FormQuote.jsx donde agregaremos el siguiente código:

components/FormQuote.jsx
export const FormQuote = () => {
  return (
    <form action="">
      <div className="notification is-success is-light">
        <strong>¡Gracias por enviar tu solicitud!</strong> Nuestro
        equipo se pondrá en contacto contigo pronto con tu cotización
        personalizada.
      </div>
      <p className="is-4 has-text-grey has-text-weight-bold">
        Información de contacto
      </p>
      <hr className="mt-2" />
      <div className="columns">
        <div className="column">
          <div className="field">
            <label htmlFor="name" className="label">
              Nombre
            </label>
            <div className="control">
              <input
                id="name"
                type="text"
                name="name"
                className="input"
              />
            </div>
            <p className="help is-danger">Debe ingresar su nombre.</p>
          </div>
        </div>
        <div className="column">
          <div className="field">
            <label htmlFor="email" className="label">
              Correo
            </label>
            <div className="control">
              <input
                id="email"
                type="text"
                name="email"
                className="input"
              />
            </div>
            <p className="help is-danger">Debe ingresar un correo.</p>
          </div>
        </div>
      </div>

      <p className="is-4 has-text-grey has-text-weight-bold mb-0 mt-5">
        Información adicional
      </p>
      <hr className="mt-2" />
      <div className="columns">
        <div className="column">
          <div className="field">
            <label htmlFor="quantity" className="label">
              Cantidad
            </label>
            <div className="control">
              <input
                id="quantity"
                type="number"
                name="quantity"
                className="input"
              />
            </div>
            <p className="help is-danger">
              Debe especificar la cantidad.
            </p>
          </div>
        </div>
        <div className="column">
          <div className="field">
            <label htmlFor="deliveryType" className="label">
              Tipo de entrega
            </label>
            <div className="select">
              <select id="deliveryType" name="deliveryType">
                <option value="">Seleccionar</option>
                <option value="delivery">Delivery</option>
                <option value="pickup">Recojo en tienda</option>
              </select>
            </div>
            <p className="help is-danger">
              Debe elegir un tipo de entrega.
            </p>
          </div>
        </div>
      </div>
      <hr className="mt-2" />
      <label className="checkbox">
        <input type="checkbox" name="tyc" /> Acepto los términos y
        condiciones
        <p className="help is-danger">
          Debe aceptar los términos y condiciones.
        </p>
      </label>
      <div className="mt-4 is-flex is-justify-content-flex-end">
        <button className="button is-success" type="submit">
          Enviar cotización
        </button>
      </div>
    </form>
  );
};

Vista previa formulario

Integración con Formik

Procedemos a instalar Formik para importar los componentes Formik y Form en nuestro componente FormQuote.jsx.

npm install formik

Luego agregamos Formik como componente padre y definimos los valores iniciales del formulario con la propiedad initialValues, también pasamos una función a la propiedad onSubmit, está recibirá los values (datos de cada campo) y se ejecutará cuando se envíe el formulario (Líneas 5-24).

Formik utiliza el patrón render props para pasar los valores del formulario a los componentes hijos, por esta razón debemos pasar una función como children del componente <Formik> (Líneas 17, 23).

Reemplazamos la etiqueta <form> por el componente <Form> (Líneas 19, 22).

components/FormQuote.jsx
import { Formik, Form } from 'formik';

export const FormQuote = () => {
  return (
    <Formik
      initialValues={{
        name: '',
        email: '',
        quantity: '',
        deliveryType: '',
        tyc: false,
      }}
      onSubmit={(values) => {
        console.log(values);
      }}
    >
      {(formikProps) =>
-     <form action="">
+     <Form>
        {/* ... Código anterior omitido por brevedad */}
-     </form>
+     </Form>
      }
    </Formik>
  );
};

El componente Form es similar a la etiqueta form con la diferencia que se conecta directamente con Formik.

// Esto...
<Form />

// es equivalente a esto...
<form onReset={formikProps.handleReset} onSubmit={formikProps.handleSubmit} {...props} />

Conectar campos del formulario con Formik

Primero haremos un destructuring de formikProps para obtener los values, el evento handleChange y handleBlur, que utilizaremos con los campos de tipo: input, select, checkbox, etc.

components/FormQuote.jsx
- {(formikProps) => (
+ {({values, handleChange, handleBlur}) => (
    <Form>
    {/* ... */}
    </Form>
  )}

Ahora modificamos cada campo del formulario para conectarlo con Formik, para ello agregamos la propiedad value con su valor equivalente en el objeto values, la propiedad onChange con el evento handleChange y la propiedad onBlur con el evento handleBlur.

components/FormQuote.jsx
  {({values, handleChange, handleBlur}) => (
    <Form>
    {/* ... */}
    <div className="control">
      <input
        id="name" type="text" name="name" className="input"
        value={values.name}
        onChange={handleChange}
        onBlur={handleBlur}
       />
    </div>

    <div className="control">
      <input
        id="email" type="email" name="email" className="input"
        value={values.email}
        onChange={handleChange}
        onBlur={handleBlur}
       />
    </div>

    <div className="control">
      <input
        id="quantity" type="number" name="quantity" className="input"
        value={values.quantity}
        onChange={handleChange}
        onBlur={handleBlur}
       />
    </div>

    <div className="control">
      <select name="deliveryType" id="deliveryType"
        value={values.deliveryType}
        onChange={handleChange}
        onBlur={handleBlur}
      >
        {/* ... */}
      </select>
    </div>

     <label className="checkbox">
        <input type="checkbox" name="tyc"
          checked={values.tyc}
          onChange={handleChange}
          onBlur={handleBlur}
        />{" "} Acepto los términos y condiciones
      </label>
    {/* ... */}
    </Form>
  )}

Otra forma de conectar los campos con Formik es utilizando el componente <Field />.

components/FormQuote.jsx
import { Formik, Form, Field } from 'formik';

export const FormQuote = () => {
  return (
    <Formik
    ...
    >
      {({values}) => (
        <Form>
        {/* ... */}
        <div className="control">
          <Field id="name" name="name" type="input" className="input" />
        </div>

        <div className="control">
          <Field id="deliveryType" name="deliveryType" as="select">
            <option value="">Seleccionar</option>
            {/* ... */}
          </Field>
        </div>
        {/* ... */}
        </Form>
      )}
    <Formik>
  )
}

Validación de campos

Para validar los campos del formulario debemos agregar la propiedad validate a Formik y pasar una función que reciba los values y retorne un objeto con los errores.

En este ejemplo validaremos que los campos no estén vacíos.

components/FormQuote.jsx
  <Formik
    ...
    validate={(values) => {
      const errors = {};
      if (!values.name) {
        errors.name = 'El campo nombre es requerido.';
      }
      if (!values.email) {
        errors.email = 'El campo correo es requerido.';
      }
      if (!values.quantity) {
        errors.quantity = 'El campo cantidad es requerido.';
      }
      if (!values.deliveryType) {
        errors.deliveryType = 'El campo tipo de entrega es requerido.';
      }
      if (!values.tyc) {
        errors.tyc = 'Debe aceptar términos y condiciones';
      }
      return errors;
    }}
    ...
  >
  {/* ...*/}
  </Formik>

Mensajes de Error

Volvemos a hacer un destructuring de formikProps para obtener el objeto errors y touched. Luego agregamos una condicional para validar si el campo se encuentra registrado dentro estos objetos y mostrar el mensaje de error.

components/FormQuote.jsx
  <Formik ... >
  {({values, handleChange, handleBlur, errors, touched}) => (
    <Form>
    {/* ... */}
    <div className="control">
      <input
        id="name" type="text" name="name" className="input"
        value={values.name}
        onChange={handleChange}
        onBlur={handleBlur}
       />
       {touched.name && errors.name && (
          <p className="help is-danger">{errors.name}</p>
        )}
    </div>
    {/* ... */}
    </Form>
  )}
  </Formik>

El objeto touched se actualiza cada vez que se realiza un evento blur en un campo, esto nos permite mostrar el mensaje de error para cada campo de forma individual.

Si no validamos con touched, entonces al momento de ejecutar un evento blur en algún campo, se mostrarán los mensajes de error de otros campos que estén vacíos.

Vista previa mensaje de error

Validación con Yup

Otra forma de validar los campos es utilizando la librería Yup que permite realizar validaciones más complejas.

npm install yup

Importamos Yup en nuestro componente FormQuote.jsx y creamos el esquema FormQuoteSchema con el siguiente código:

components/FormQuote.jsx
import * as Yup from 'yup';
import { Formik, Form, Field } from 'formik';

const FormQuoteSchema = yup.object().shape({
  name: yup.string().required("Debe ingresar su nombre."),
  email: yup.string().email("Ingrese un correo valido.").required("Debe ingresar un correo."),
  quantity: yup.number().moreThan(0).required("Debe especificar la cantidad."),
  deliveryType: yup.string().required("Debe elegir un tipo de entrega"),
  tyc: yup.boolean().oneOf([true], "Debe aceptar los términos y condiciones."),
});

Ahora, retiramos la propiedad validate y agregamos la propiedad validationSchema a Formik donde pasaremos el esquema creado.

components/FormQuote.jsx
import * as Yup from 'yup';
import { Formik, Form, Field } from 'formik';

const FormQuoteSchema = yup.object().shape({...})

export const FormQuote = () => {
  return (
    <Formik
      initialValues={...}
-     validate={(values) => ... }
+     validationSchema={FormQuoteSchema}
    >
      {/* ...*/}
    </Formik>
  )
}

Envío del formulario (Submit)

Ha llegado el momento de enviar los datos del formulario.

Pero antes, agregaremos un estado loading al botón Enviar para indicar que el formulario esta ejecutando el submit.

Para lograr esto, obtenemos la propiedad isSubmitting de formikProps y la utilizamos para hacer agregar la clase css is-loading al botón Enviar.

components/FormQuote.jsx
<Formik>
  {({values, handleChange, handleBlur, errors, touched, isSubmitting}) => (
    <Form>
    {/* ... */}
    <button
-     className="button is-success is-loading" type="submit">
+     className={`button is-success ${isSubmitting ? "is-loading" : ""}`.trim()}
    >
      Enviar cotización
    </button>
    {/* ... */}
    </Form>
  )}
<Formik>

Mensaje de Éxito

Ahora vamos a mostrar un mensaje y hacer un reset de los campos cuando el formulario se haya enviado correctamente.

En primer lugar, comenzamos agregando un estado success con el hook useState y lo inicializamos en false (Línea 4).

Luego, integramos la propiedad onSubmit en el componente Formik, pasando una función que recibe los values y formikProps. Aquí simulamos el envío del formulario con un setTimeout de 3 segundos.Dentro de este lapso, actualizamos el estado success a true y hacemos un reset de los campos utilizando formikProps.resetForm() (Líneas 9-14).

Para finalizar, aplicamos una condición para mostrar el mensaje de éxito unicamente cuando el estado success es true (Líneas 18-23).

components/FormQuote.jsx
import { Formik, Form } from 'formik';

export const FormQuote = () => {
  const [success, setSuccess] = useState(false);

  return (
    <Formik
    ...
    onSubmit={(values, formikProps) => {
      setTimeout(() => {
        setSuccess(true);
        formikProps.resetForm();
      }, 3000);
    }}
    >
      {({values, handleChange, handleBlur, errors, touched, isSubmitting}) => (
        <Form>
          {success && (
            <div className="notification is-success is-light">
              <strong>¡Gracias por enviar tu solicitud!</strong> Nuestro equipo se pondrá
              en contacto contigo pronto con tu cotización personalizada.
            </div>
          )}
        {/* ... */}
        </Form>
      )}
    <Formik>
  )
}

Vista previa formulario

Demo

Aquí puedes ver el resultado final:

¡Gracias por seguir este tutorial! Espero que haya sido útil para ti. Que tengas un día maravilloso! 🌞