DEV Community

Babatunde Adeniran
Babatunde Adeniran

Posted on

Formik & React (Part 2): Enhancing Validation & Error Handling

In our previous article, we introduced Formik and explored how the useFormik hook simplifies form state management, form submission, and validation. However, there are still a few crucial improvements needed to enhance the user experience.

While we have implemented basic validation, there’s room for improvement. Currently, our form does not display error messages or keep track of fields that have already been visited—both of which are essential for a better user experience.

In this article, we’ll address these issues and take our form handling to the next level.

Let’s dive in!

Displaying Error Messages

We already have a validate function that checks for errors in our form fields (name, email, and quantity) and returns corresponding error messages based on our validation rules and conditions. However, we still need a way to display these error messages to the user.

As we’ve established, the formik object returned by the useFormik hook contains various helper methods and useful properties for managing our form. So far, we’ve explored initialValues, onSubmit, and validate.

The validate function helps us retrieve error messages, but how do we access them? Recall that formik.values contains key-value pairs for our name, email, and quantity form fields. Similarly, Formik provides an errors property, which follows the same structure as formik.values—it’s an object with key-value pairs for our form fields, holding their corresponding validation errors.

Let’s log formik.errors to the console and see what it contains.

  const formik = useFormik({
    initialValues,
    onSubmit,
    validate
  })
  console.log("Form errors", formik.errors)
Enter fullscreen mode Exit fullscreen mode

Typically, formik.errors returns an empty object when the page mounts.

 raw `formik.errors` endraw

However, when we alter the initialValues, the errors object is populated with keys that are similar to that of formik.values from our previous article. We would also see that the value of each error field is the error message we defined in our validate function.

 raw `formik.errors` endraw

What Formik does under the hood is to run the validate function when the form field changes. The formik.errors object is then populated with error messages based on the rules outlined in the validate function.

Next, we conditionally render error messages from the formik.errors object in JSX.

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



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

const onSubmit = (values) => {
  console.log(values)
}
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}
          />
          {formik.errors.name ? (
            <div className="error">{formik.errors.name}</div>
          ) : null}
        </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}
          />
          {formik.errors.email ? (
            <div className="error">{formik.errors.email}</div>
          ) : null}
        </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}
          />
          {formik.errors.quantity ? (
            <div className="error">{formik.errors.quantity}</div>
          ) : null}
        </div>

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

Enter fullscreen mode Exit fullscreen mode

We would also add an error class to our order.css file.

.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;
  }

  .error {
    color: red;
    margin-top: 5px;
  }
Enter fullscreen mode Exit fullscreen mode

Validation errors

Although the error messages are displayed correctly, there is still an issue to fix. Currently, error messages for all three fields appear even when we interact with just one input field. This is not an ideal user experience, let's correct it.

Visited Fields

Although the error messages are visible to users, we have to keep track of visited fields so that error messages are not displayed on inputs or fields that are yet to be visited. As it stands, every keyboard event triggers the validate function to be called and this means the form.errors object containing the error messages for name, email, and quantity will be populated at every instance. We only want to display error messages for the field the user has interacted with, and it even makes more sense to display error messages for a field after the user has finished typing in that field.

The onBlur prop tracks which fields have been visited. Adding it to our input fields will resolve this issue. Formik also provides a helper method called handleBlur, which we pass to the onBlur prop.

Formik saves the details of already visited fields in the touched object. The touched object has a similar structure to the formik.errors and formik.values objects.

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



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

const onSubmit = (values) => {
  console.log(values)
}
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
  })

  console.log("Visited form fields", formik.touched)

  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}
            onBlur={formik.handleBlur}
            value={formik.values.name}
          />
          {formik.errors.name ? (
            <div className="error">{formik.errors.name}</div>
          ) : null}
        </div>

        <div className="form-group">
          <label htmlFor='email'>Email:</label>
          <input
            type="email"
            name="email"
            id='email'
            required
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.email}
          />
          {formik.errors.email ? (
            <div className="error">{formik.errors.email}</div>
          ) : null}
        </div>

        <div className="form-group">
          <label htmlFor='quantity'>Order Quantity:</label>
          <input
            type="number"
            name="quantity"
            id='quantity'
            required
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.quantity}
          />
          {formik.errors.quantity ? (
            <div className="error">{formik.errors.quantity}</div>
          ) : null}
        </div>

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

Enter fullscreen mode Exit fullscreen mode

If we check the logs for the visited fields on page load, we should see an empty object indicating that none of the fields have been visited.

Visited fields

Let's click in and out of the email field

Touched field

We can see that the touched object is no longer empty—the email field has been marked as visited. The same applies to the name and quantity fields when touched. The details of the formik.touched object can then be used to dynamically render error messages.

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



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

const onSubmit = (values) => {
  console.log(values)
}
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}
            onBlur={formik.handleBlur}
            value={formik.values.name}
          />
          {formik.touched.name && formik.errors.name ? (
            <div className="error">{formik.errors.name}</div>
          ) : null}
        </div>

        <div className="form-group">
          <label htmlFor='email'>Email:</label>
          <input
            type="email"
            name="email"
            id='email'
            required
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.email}
          />
          {formik.touched.email && formik.errors.email ? (
            <div className="error">{formik.errors.email}</div>
          ) : null}
        </div>

        <div className="form-group">
          <label htmlFor='quantity'>Order Quantity:</label>
          <input
            type="number"
            name="quantity"
            id='quantity'
            required
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.quantity}
          />
          {formik.touched.quantity && formik.errors.quantity ? (
            <div className="error">{formik.errors.quantity}</div>
          ) : null}
        </div>

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

Enter fullscreen mode Exit fullscreen mode

There are other ways we can define our form validation rules. We will look at that next.

Top comments (0)