Form Handling with Formik and Yup in React: A Comprehensive Guide

Form Handling with Formik and Yup in React: A Comprehensive Guide

As a developer, it is important to build forms that provide a good user experience making it easy for users to interact with the form. An important feature in modern-day forms is the ability to validate and provide feedback as the user interacts with the form.

Form validation ensures you get the correct data in the right form, and protect the user’s data by enforcing strong passwords. You’re also able to protect your application from malicious attacks in cases where unprotected forms are exploited.

In this tutorial, you will build a form using React, and Formik to capture the form data. You’ll also learn to validate form data using Yup to validate the form data.

Note: This tutorial assumes that you are familiar with React, React hooks, ES6 concepts like destructing and spread syntax, and Tailwind CSS.

If you're eager to directly access the finalized code and see it in action, you can locate it within the CodePen sandbox below.

Setting up project

For the tutorial, I've already set up a simple sign-up form.

  • Clone the project by running the command below in your terminal. Alternatively, you can download the project here.
git clone https://github.com/Kachielite/formik-form
  • Install all dependencies by running the command below in the project folder.
cd formik-forms
npm install
  • Enter the command to install Formik and Yup.
npm install formik yup
  • Once the installation is done, start the project using the command below
npm start

This should automatically open the project on your browser. If this does not happen, open your browser and paste the link localhost:3000

You should see the form below displayed in your browser.

Creating Your First Form

  • In the project folder, navigate to the src directory and there you will find the file App.js. It contains a simple sign-up form with no validation containing the code below.
import React from "react";

const App = () => {
  return (
    <div className="container">
      <form>
        <h1 className="">Sign up!!</h1>
        <div className="form_input_container">
          <label htmlFor="first_name">First Name</label>
          <input
            className="form_input"
            id="first_name"
            type="text"
            placeholder="Enter your first name"
          />
        </div>
        <div className="form_input_container">
          <label htmlFor="first_name">Last Name</label>
          <input
            className="form_input"
            id="last_name"
            type="text"
            placeholder="Enter your last name"
          />
        </div>
        <div className="form_input_container">
          <label htmlFor="first_name">Email Address</label>
          <input
            className="form_input"
            id="email"
            type="text"
            placeholder="Enter your email"
          />
        </div>
        <div className="form_btn_container">
          <button className="btn" type="submit">
            Sign In
          </button>
        </div>
      </form>
      <p>&copy;2020 All rights reserved.</p>
    </div>
  );
};

export default App;
  • At the top of the App.js, import Formik into the project.
import React from "react";
import { useFormik } from 'formik';

const App = () => {

   const formik = useFormik({
     initialValues: {
       firstName: '',
       lastName: '',
       email: '',
     },
   });

    return(
        <div className="container">
        ...
        </div>
    );
}

export default App;

We pass in the initialValues object to the useFormik hook as arguments. This hook will return the following helper method and form state.

i. values: An object containing the form's current values.

ii. touched: An object that keeps track of visited fields

iii. errors: An object of errors with the same key as the initialValues/values

iii. handleChange: This function is invoked with each keystroke made while the input field is in focus, capturing keyboard input dynamically.

iv. handleBlur: Whenever you move away from the input field, the handleBlur function is activated, responding to the loss of focus.

v. handleSubmit: This handles the form submission action

  • Add the handleChange helper method to the <input/> tags.
...

const App = () => {
    return(
        <div className="container">
          <form>
            <h1 className="">Sign up!!</h1>
            <div className="form_input_container">
              <label htmlFor="first_name">First Name</label>
              <input
                className="form_input"
                id="first_name"
                type="text"
                placeholder="Enter your first name"
                onChange={formik.handleChange}
                value={formik.values.first_name}
              />
            </div>
            <div className="form_input_container">
              <label htmlFor="first_name">Last Name</label>
              <input
                className="form_input"
                id="last_name"
                type="text"
                placeholder="Enter your last name"
                onChange={formik.handleChange}
                value={formik.values.last_name}
              />
            </div>
            <div className="form_input_container">
              <label htmlFor="first_name">Email Address</label>
              <input
                className="form_input"
                id="email"
                type="text"
                placeholder="Enter your email"
                onChange={formik.handleChange}
                value={formik.values.email_name}
              />
            </div>
            <div className="form_btn_container">
              <button className="btn" type="submit">
                Sign In
              </button>
            </div>
          </form>
          <p>&copy;2020 All rights reserved.</p>
        </div>
    );
};

export default App;
  • Verify that the form data is captured by adding the console logging the values this way.
console.log("initialValues", formik.values)

Open your browser dev tool and on the console, you should have the values logged as you input data into the form.

Form Validation with Yup

Yup seamlessly integrates with Formik to produce an object whose keys match values and intialValues.

  • Import Yup into our project and add a validationSchema to the useFormik hook. Yup helps a lot with schemas that are extremely expressive, intuitive (since they mirror your values), and reusable. It provides you with an easy way to perform form validation using a few lines of code.
import React from "react";
import { useFormik } from 'formik';
import * as Yup from 'yup';

const App = () => {

   const formik = useFormik({
     initialValues: {
       first_name: '',
       last_name: '',
       email: '',
     },
     validationSchema: Yup.object({
       first_name: Yup.string().required('Required'),
       last_name: Yup.string().required('Required'),
       email: Yup.string().email('Invalid email address').required('Required'),
     })
   });

    return(
        <div className="container">
        ...
        </div>
    );
}

export default App;

Now that your validation schema is ready, add error feedback below each input to notify the user when wrong data is provided. To do this, use the errors object returned by the useFormik hook.

There are two different ways of achieving this.

  • add a red border to the input with the help of a custom class called error-input.

  • add a p tag below each input and add the custom class error-text.

...

const App = () => {
    ...
    return (
        <div className="container">
          <form>
            <h1 className="">Sign up!!</h1>
            <div className="form_input_container">
              <label htmlFor="first_name">First Name</label>
              <input
                className={`form_input ${
                  formik.errors.first_name &&
                  "error-input"
                }`}
                id="first_name"
                type="text"
                placeholder="Enter your first name"
                onChange={formik.handleChange}
                value={formik.values.first_name}
              />
              {formik.errors.first_name && (
                <p className="error-text">{formik.errors.first_name}</p>
              )}
            </div>
            <div className="form_input_container">
              <label htmlFor="first_name">Last Name</label>
              <input
                className={`form_input ${
                  formik.errors.last_name &&
                  "error-input"
                }`}
                id="last_name"
                type="text"
                placeholder="Enter your last name"
                onChange={formik.handleChange}
                value={formik.values.last_name}
              />
              {formik.errors.last_name && (
                <p className="text-red-500 text-xs italic">
                  {formik.errors.last_name}
                </p>
              )}
            </div>
            <div className="form_input_container">
              <label htmlFor="email">Email Address</label>
              <input
                className={`form_input ${
                   formik.errors.email && "error-input"
                }`}
                id="email"
                type="email"
                placeholder="Enter your email"
                onChange={formik.handleChange}
                value={formik.values.email_name}
              />
              {formik.errors.email && (
                <p class="error-text">{formik.errors.email}</p>
              )}
            </div>
            <div className="form_btn_container">
              <button className="btn" type="submit">
                Sign In
              </button>
            </div>
          </form>
          <p>&copy;2023 All rights reserved.</p>
        </div>
    );
}

export default App;
  • To verify that this works, add a function to handle submission in your form and as an argument to the useFormik hook.
...

const App = () => {

   const formik = useFormik({
       ...
    onSubmit: (values) => {
      alert(JSON.stringify(values, null, 2));
    },
   });

    return(
        <div className="container">
            <form onSubmit={formik.handleSubmit}>
               ...
            </form>
        </div>
    );
}

export default App;

Result: When you try submitting an empty form, the input borders turn red and display an error message.

Although the form validation works, it displays all errors even on fields we haven’t visited. Here’s how to fix that.

Formik keeps track of the fields visited in an object called touched which contains the same keys as the object errors and values.

  • With that in mind, add the touched and handleBlur helper function to each input. This will enable the form to only display an error below a field that has been visited.
...

const App = () => {
  ...
  return (
    <div className="container">
      <form onSubmit={formik.handleSubmit}>
        <h1 className="">Sign up!!</h1>
        <div className="form_input_container">
          <label htmlFor="first_name">First Name</label>
          <input
            className={`form_input ${
              formik.touched.first_name &&
              formik.errors.first_name &&
              "error-input"
            }`}
            id="first_name"
            type="text"
            placeholder="Enter your first name"
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.first_name}
          />
          {formik.touched.first_name && formik.errors.first_name && (
            <p className="error-text">{formik.errors.first_name}</p>
          )}
        </div>
        <div className="form_input_container">
          <label htmlFor="first_name">Last Name</label>
          <input
            className={`form_input ${
              formik.touched.last_name &&
              formik.errors.last_name &&
              "error-input"
            }`}
            id="last_name"
            type="text"
            placeholder="Enter your last name"
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.last_name}
          />
          {formik.touched.last_name && formik.errors.last_name && (
            <p className="text-red-500 text-xs italic">
              {formik.errors.last_name}
            </p>
          )}
        </div>
        <div className="form_input_container">
          <label htmlFor="email">Email Address</label>
          <input
            className={`form_input ${
              formik.touched.email && formik.errors.email && "error-input"
            }`}
            id="email"
            type="email"
            placeholder="Enter your email"
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.email_name}
          />
          {formik.touched.email && formik.errors.email && (
            <p class="error-text">{formik.errors.email}</p>
          )}
        </div>
        <div className="form_btn_container">
          <button className="btn" type="submit">
            Sign In
          </button>
        </div>
      </form>
      <p>&copy;2023 All rights reserved.</p>
    </div>
  );

}

export default App;

Once you’re done, here’s how your form validation should display.

Here’s the full code

import React from "react";
import { useFormik } from "formik";
import * as Yup from "yup";

const App = () => {
  const formik = useFormik({
    initialValues: {
      first_name: "",
      last_name: "",
      email: "",
    },
    validationSchema: Yup.object({
      first_name: Yup.string().required("First name is required."),
      last_name: Yup.string().required("Last name is required."),
      email: Yup.string()
        .email("Please provide a valid email address")
        .required("Email is required."),
    }),
    onSubmit: (values) => {
      alert(JSON.stringify(values, null, 2));
    },
  });

  return (
    <div className="container">
      <form onSubmit={formik.handleSubmit}>
        <h1 className="">Sign up!!</h1>
        <div className="form_input_container">
          <label htmlFor="first_name">First Name</label>
          <input
            className={`form_input ${
              formik.touched.first_name &&
              formik.errors.first_name &&
              "error-input"
            }`}
            id="first_name"
            type="text"
            placeholder="Enter your first name"
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.first_name}
          />
          {formik.touched.first_name && formik.errors.first_name && (
            <p className="error-text">{formik.errors.first_name}</p>
          )}
        </div>
        <div className="form_input_container">
          <label htmlFor="first_name">Last Name</label>
          <input
            className={`form_input ${
              formik.touched.last_name &&
              formik.errors.last_name &&
              "error-input"
            }`}
            id="last_name"
            type="text"
            placeholder="Enter your last name"
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.last_name}
          />
          {formik.touched.last_name && formik.errors.last_name && (
            <p className="text-red-500 text-xs italic">
              {formik.errors.last_name}
            </p>
          )}
        </div>
        <div className="form_input_container">
          <label htmlFor="email">Email Address</label>
          <input
            className={`form_input ${
              formik.touched.email && formik.errors.email && "error-input"
            }`}
            id="email"
            type="email"
            placeholder="Enter your email"
            onChange={formik.handleChange}
            onBlur={formik.handleBlur}
            value={formik.values.email_name}
          />
          {formik.touched.email && formik.errors.email && (
            <p class="error-text">{formik.errors.email}</p>
          )}
        </div>
        <div className="form_btn_container">
          <button className="btn" type="submit">
            Sign In
          </button>
        </div>
      </form>
      <p>&copy;2023 All rights reserved.</p>
    </div>
  );
};

export default App;

Handling Form Submission

To handle form validation, you have to pass your submission function as an argument to the useFormik hook as seen above.

This function makes a POST request with the required request body from the form to the backend server. In this guide, the form data will be solely displayed in the browser alert dialogue.

In return, this submit function is invoked when the form is submitted using the helper function handleSubmit.

Additional Resources

For in-depth knowledge and further exploration, here are some additional resources:

Feel free to explore tutorials, videos, and community discussions on these platforms to deepen your understanding and master the art of form handling with Formik and Yup. Happy coding!