DEV Community

Babatunde Adeniran
Babatunde Adeniran

Posted on

Formik & React: Writing Cleaner, More Efficient Forms

The importance of forms in business applications cannot be overemphasized. We use forms to accomplish many tasks and facilitate various processes, such as user registration, user sign-in, feedback collection, and order creation. Because forms are crucial to most web interactions, it is essential to ensure a seamless user experience through effective development.

When creating forms, developers take on numerous responsibilities, including field validation, efficient handling of form data, comprehensive error management, and an optimized form submission process.

In this article, I will explain how we can achieve all these with Formik, a highly useful library for managing forms in React and React Native.

Formik simplifies form handling by managing form data, validation, submission, and error messages efficiently. While React provides multiple approaches to handling forms, Formik abstracts the complex and tedious parts, allowing developers to build advanced forms with ease. It is both scalable and performant, making it an excellent choice for form management in React applications.

We start with a simple presentational React.js form component without any functionality. The goal is to implement form state management, handle form submission, validate inputs, and display error messages. The form is designed for submitting orders for an imaginary company, Love & Light Pizzas Ltd. The order submission form has three fields: name, email, and order quantity. It will also include a submit button.

import "./order.css"

export const OrdersForm = () => {
  return (
    <div className="form-container">
      <h2>Order Submission Form</h2>
      <form >
        <div className="form-group">
          <label htmlFor='name'>Name:</label>
          <input
            type="text"
            name="name"
            id='name'
            required
          />
        </div>

        <div className="form-group">
          <label htmlFor='email'>Email:</label>
          <input
            type="email"
            name="email"
            id='email'
            required
          />
        </div>

        <div className="form-group">
          <label htmlFor='quantity'>Order Quantity:</label>
          <input
            type="number"
            name="quantity"
            id='quantity'
            required
          />
        </div>

        <button type="submit" className="submit-btn">
          Submit Order
        </button>
      </form>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode
.form-container {
    width: 350px;
    margin: 40px auto;
    padding: 20px;
    border: 1px solid #ccc;
    border-radius: 8px;
    background-color: #f9f9f9;
    text-align: center;
  }

  h2 {
    margin-bottom: 20px;
  }

  .form-group {
    margin-bottom: 15px;
    text-align: left;
  }

  label {
    display: block;
    font-weight: bold;
    margin-bottom: 5px;
  }

  input {
    width: 100%;
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  .submit-btn {
    width: 100%;
    padding: 10px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 16px;
    cursor: pointer;
  }

  .submit-btn:hover {
    background-color: #0056b3;
  }

Enter fullscreen mode Exit fullscreen mode

After installing Formik, our next step is to import the useFormik hook which the library provides. Since hooks are simply functions, they need to be called within the component. The useFormik hook returns an object containing a variety of methods and properties that can be used within the form. With this Formik object, it becomes easier to achieve our aim of handling form submission, managing the state of the form, and validating input error messages.

FORM STATE MANAGEMENT

We already know that state is an object that determines how a component renders and behaves. At the moment, our form component is an uncontrolled component, which means it is a traditional HTML form input whose value we cannot manage directly. Our order form has three fields, but as it stands, we are not keeping track of the form field values. When we enter something into the input fields, the field value changes. In React, when there are changes in values, we use state variables to keep track of those changes. So, we need state variables for the name, email, and order quantity inputs. Together, these are referred to as the form state.

In Formik, the form state is an object that maintains the value of individual form fields. Therefore, it is crucial to understand how Formik helps manage the form state when form field values change.

We first pass a property called initial values in the object configuration. It is an object that contains the initial values of our form fields. The properties of the initialValues object must correspond with the name attribute of our form inputs.

To track changes in individual form inputs, we add an onChange and value prop to each input. This is similar to basic form handling in React.js, where we assign the state property as the input’s value and handle the onChange event by creating an event handler. With Formik, we use formik.handleChange to handle this event. Additionally, we can sync the value of the input with the state property by accessing formik.values.nameAttribute, as shown below.

import "./order.css"
import { useFormik } from "formik"

export const OrdersForm = () => {
  const formik = useFormik({
    initialValues: {
      name: "",
      email: "",
      quantity: 0
    }
  })
  return (
    <div className="form-container">
      <h2>Order Submission Form</h2>
      <form >
        <div className="form-group">
          <label htmlFor='name'>Name:</label>
          <input
            type="text"
            name="name"
            id='name'
            required
            onChange={formik.handleChange}
            value={formik.values.name}
          />
        </div>

        <div className="form-group">
          <label htmlFor='email'>Email:</label>
          <input
            type="email"
            name="email"
            id='email'
            required
            onChange={formik.handleChange}
            value={formik.values.email}
          />
        </div>

        <div className="form-group">
          <label htmlFor='quantity'>Order Quantity:</label>
          <input
            type="number"
            name="quantity"
            id='quantity'
            required
            onChange={formik.handleChange}
            value={formik.values.quantity}
          />
        </div>

        <button type="submit" className="submit-btn">
          Submit Order
        </button>
      </form>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

When we log the formik.values object in the console, we would see that it is structured such that the keys are the name attributes of the form fields, and as we type into individual fields, the changes are reflected in each input value.

Log result of form input values

Log result of form input values

We see that at every point in time, formik.values reflects the application state.

HANDLING FORM SUBMISSION

So far, we have discussed how the form values are tracked and extracted. However, we have not yet covered how the values are handled when the user clicks the submit button. There are two steps involved in form submission with Formik.

The first step is to specify the onSubmit handler on the <form> tag, just as we listened to change events on form inputs using the onChange handler. Typically, the Formik helper method we previously defined provides a method for handling form submission, called the handleSubmit method.

For the second step, we return to our useFormik hook configuration and pass a second property called onSubmit. By default, the onSubmit method receives the state of the form as its argument. The values argument in the onSubmit method is a reference to formik.values. The onSubmit method is triggered as soon as the submit button is clicked.

import "./order.css"
import { useFormik } from "formik"

export const OrdersForm = () => {
  const formik = useFormik({
    initialValues: {
      name: "",
      email: "",
      quantity: 0
    },
    onSubmit: (values) => {
      console.log(values, "Form data")
    }
  })

  return (
    <div className="form-container">
      <h2>Order Submission Form</h2>
      <form onSubmit={formik.handleSubmit}>
        <div className="form-group">
          <label htmlFor='name'>Name:</label>
          <input
            type="text"
            name="name"
            id='name'
            required
            onChange={formik.handleChange}
            value={formik.values.name}
          />
        </div>

        <div className="form-group">
          <label htmlFor='email'>Email:</label>
          <input
            type="email"
            name="email"
            id='email'
            required
            onChange={formik.handleChange}
            value={formik.values.email}
          />
        </div>

        <div className="form-group">
          <label htmlFor='quantity'>Order Quantity:</label>
          <input
            type="number"
            name="quantity"
            id='quantity'
            required
            onChange={formik.handleChange}
            value={formik.values.quantity}
          />
        </div>

        <button type="submit" className="submit-btn">
          Submit Order
        </button>
      </form>
    </div>
  )
}


Enter fullscreen mode Exit fullscreen mode

Form submission values

We see that the logs after the submit button is clicked are in sync with the content of the form.

HANDLING FORM VALIDATION

As it stands, we can manage the state of our order form and submit data by clicking a button. However, we have not yet validated our form fields to filter form values based on established rules.

With Formik, we can define a validation function assigned to the validate property, which will be our third property in the useFormik hook configuration. Like the onSubmit property, the validate function receives the values object as an argument by default. There are certain conditions that the validate function must satisfy, one of these is that it must return an object. We assign this object to a variable and return it.

  validate: (values) => {
      const errors: any = {}
      return errors
    }
Enter fullscreen mode Exit fullscreen mode

Another condition that must be fulfilled is that the keys of the values and errors objects must be in sync. Currently, we have values.name, values.email, and values.quantity. Similarly, we will have errors.name, errors.email, and errors.quantity. Additionally, the values of the keys defined in the errors object must be strings that convey the correct error message.

import "./order.css"
import { useFormik } from "formik"

export const OrdersForm = () => {
  const formik = useFormik({
    initialValues: {
      name: "",
      email: "",
      quantity: 0
    },
    onSubmit: (values) => {
      console.log(values, "Form data")
    },
    validate: (values) => {
      const errors = {}
      if (!values.name) {
        errors.name = "Name is required"
      }
      if (!values.email) {
        errors.email = "Email is required"
      } else if (
        !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)
      ) {
        errors.email = "Invalid email address"
      }
      if (!values.quantity) {
        errors.quantity = "Quantity is required"
      }
      return errors
    }
  })

  return (
    <div className="form-container">
      <h2>Order Submission Form</h2>
      <form onSubmit={formik.handleSubmit}>
        <div className="form-group">
          <label htmlFor='name'>Name:</label>
          <input
            type="text"
            name="name"
            id='name'
            required
            onChange={formik.handleChange}
            value={formik.values.name}
          />
        </div>

        <div className="form-group">
          <label htmlFor='email'>Email:</label>
          <input
            type="email"
            name="email"
            id='email'
            required
            onChange={formik.handleChange}
            value={formik.values.email}
          />
        </div>

        <div className="form-group">
          <label htmlFor='quantity'>Order Quantity:</label>
          <input
            type="number"
            name="quantity"
            id='quantity'
            required
            onChange={formik.handleChange}
            value={formik.values.quantity}
          />
        </div>

        <button type="submit" className="submit-btn">
          Submit Order
        </button>
      </form>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

Now, let's refactor this code to improve the readability of the userFormik hook.

import "./order.css"
import { useFormik } from "formik"



const initialValues = {
  name: "",
  email: "",
  quantity: 0
}

const onSubmit = (values) => {
  console.log(values, "Form data")
}
const validate = (values) => {
  const errors = {}
  if (!values.name) {
    errors.name = "Name is required"
  }
  if (!values.email) {
    errors.email = "Email is required"
  } else if (
    !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)
  ) {
    errors.email = "Invalid email address"
  }
  if (!values.quantity) {
    errors.quantity = "Quantity is required"
  }
  return errors
}

export const OrdersForm = () => {
  const formik = useFormik({
    initialValues,
    onSubmit,
    validate

  })

  return (
    <div className="form-container">
      <h2>Order Submission Form</h2>
      <form onSubmit={formik.handleSubmit}>
        <div className="form-group">
          <label htmlFor='name'>Name:</label>
          <input
            type="text"
            name="name"
            id='name'
            required
            onChange={formik.handleChange}
            value={formik.values.name}
          />
        </div>

        <div className="form-group">
          <label htmlFor='email'>Email:</label>
          <input
            type="email"
            name="email"
            id='email'
            required
            onChange={formik.handleChange}
            value={formik.values.email}
          />
        </div>

        <div className="form-group">
          <label htmlFor='quantity'>Order Quantity:</label>
          <input
            type="number"
            name="quantity"
            id='quantity'
            required
            onChange={formik.handleChange}
            value={formik.values.quantity}
          />
        </div>

        <button type="submit" className="submit-btn">
          Submit Order
        </button>
      </form>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

In the next article, we will cover how to display these error messages, identify visited fields, define a validation schema with Yup, and reduce boilerplate to improve reusability.

Top comments (0)