DEV Community

Cover image for Building Forms with Formik and React - Part 2
Farley Knight
Farley Knight

Posted on

Building Forms with Formik and React - Part 2

About this post

In the previous post of this series, we built a form using React only. We used React's state to store the values held by each <input>, making them React-controlled components. The resulting form, which was functional for our purposes, included a lot of boilerplate. But, we can do much better!

We're going to introduce a library called Formik that should hopefully make building forms much less painful.

Adding Formik to the Project

To add Formik to our project, we will use the npm install command, as we did for Bootstrap in the last post.

$ npm install --save formik

And make sure that formik is now in your list of dependencies.

/* Part of package.json  */
"dependencies": {
  "@testing-library/jest-dom": "^4.2.4",
  "@testing-library/react": "^9.3.2",
  "@testing-library/user-event": "^7.1.2",
  "bootstrap": "^4.4.1",
  "formik": "^2.1.4",  /* Make sure you see this line */
  "react": "^16.12.0",
  "react-dom": "^16.12.0",
  "react-scripts": "3.4.0"
},

Rewriting LoginForm

In the original version of LoginForm, we had a lot of boilerplate involved. We needed to set up state to handle current form state, form validation and error messages. In Formik, there is built-in support for handling state. Validation will still be specified by us, but Formik has a prop we can set for this validation function.

We're not going to duplicate the entire LoginForm class from the previous post, but we should touch on its interface. The component had the following methods, with summaries of what they did:

// Class structure for LoginForm
class LoginForm extends React.Component {
  constructor(props) {
    /* In this method we initialized `state` for the form values,
       the field validity, and their errors. */
    ...
  }

  onSubmit(event) {
    /* Iterated over the form values and checked if they were valid */
    ...
  }

  validate(name, value) {
    /* Checked if a given value was valid, based on the field name */
    ...
  }

  onChange(event) {
    /* Because this is a controlled component, we need to update our
       form values whenever they change */
    ...
  }

  render() {
    /* The HTML for our component */
    ...
  }
}

By using Formik, we no longer need to do our own onChange updating. The validation step is handled by Formik, so we don't need to add that to our onSubmit method. Finally, Formik handles initial values, stores validity, and lets the user check errors via a validate method.

Rewriting our LoginForm using Formik will make building this form much less complicated, and much more routine.

Import Formik

In order to use Formik, we need to import it. Here's what the top of LoginForm.js should look like.

// Top of LoginForm.jsx
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from "formik";

class LoginForm extends React.Component {
  ...
}

Rewriting render

We're going to use our new Formik component to rewrite the render method of our LoginForm. The main <form> tag will be replaced by <Formik>. In turn <Formik> is passed a function that renders the <Form>. Note that the 'F' is capitalized, since this component is specific to Formik.

The <Formik> component requires a few props be set to be useable:

  • initialValues - Determines the initial state of the form.
  • validate - A function which validates the form, and updates any errors.
  • onSubmit (optional) - A function we want to call after validation, but before final submit. This is likely where you would send your payload to the HTTP server.

Comments are given inline, pointing out important usage of <Form>, <Field>, and <ErrorMessage>.

class LoginForm extends React.Component {
  ...
  render() {
    return (
      <div className="container">
        <div className="row justify-content-center">
          <div className="col-lg-6">
            <div className="col-lg-12">
              /* Add new <Formik> component with two new methods that we have
                 not written yet: `initialValues` and `validate` */
              <Formik
                initialValues={this.initialValues()}
                validate={this.validate.bind(this)}>
                {
                  props => (
                    /* Our <Form> component is our main container */
                    <Form>
                      <div className="form-group">
                        <label htmlFor="email">Email</label>
                        /* This <Field> handles state change for the <input> */
                        <Field
                          type="email"
                          name="email"
                          placeholder="Enter email"
                          className={`form-control ${props.errors.email ? "is-invalid" : ""}`}
                        />
                        /* Formik handles error messages for us with this component. */
                        <ErrorMessage
                          component="div"
                          name="email"
                          className="invalid-feedback"
                        />
                      </div>

                      /* The changes to the password field are similar */
                      <div className="form-group">
                        <label htmlFor="password">Password</label>
                        <Field
                          type="password"
                          name="password"
                          placeholder="Enter password"
                          className={`form-control ${props.errors.password ? "is-invalid" : ""}`}
                        />
                        <ErrorMessage
                          component="div"
                          name="password"
                          className="invalid-feedback"
                        />
                      </div>

                      <button type="submit" className="btn btn-primary btn-block">
                        Log in
                      </button>                      
                    </Form>
                  )
                }
              </Formik>
            </div>
          </div>
        </div>
      </div>      
    );
  }
}

Adding initialValues and validate

The biggest change to our form is in the render method. We are close to done with our rewrite, but we still have a two more methods: initialValues and validate. Below are the implementations that should work for our needs:

class LoginForm extends React.Component {
  initialValues() {
    return {
      email: "",
      password: ""
    }
  }

  validate(values) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
    let errors = {};

    if (values.email === "") {
      errors.email = "Email is missing";
    } else if (!emailRegex.test(values.email)) {
      errors.email = "Email is not in the expected email address standard format";
    }
    if (values.password === "") {
      errors.password = "Password is missing";
    } else if (values.password.length < 6) {
      errors.password = "Password must be 6 characters at minimum";
    }
    return errors;    
  }
  ...
}

The initialValues method returns a new JavaScript object with empty strings for email and password. The validate method has changed to taking a JavaScript object with the current form values. We handle not only the two previous validations from our React-only form, but also verify that these fields are not empty, letting the user know they are missing.

We're now ready to test our the refactored form.

Testing it Out

After making those changes, we should have a working login page again.

Alt Text

When using this form, you'll notice that the error message for email appears immediately after switching from email to password. Also, we are checking for multiple validations, not just required or email format.

Alt Text

Our new form has the same functionality as the previous React form, which means our refactor was successful! However, we can go further. In particular, the validate method can be refactored further. Formik has built in support for another library called Yup, which allows us to describe the fields of our form in a declarative way.

Using Yup for Validation

Before we get into what Yup can do, let's first add it to our package.json.

$ npm install --save yup

Verify you have the correct package:

/* Part of package.json */
"dependencies": {
  "@testing-library/jest-dom": "^4.2.4",
  "@testing-library/react": "^9.3.2",
  "@testing-library/user-event": "^7.1.2",
  "bootstrap": "^4.4.1",
  "formik": "^2.1.4",
  "react": "^16.12.0",
  "react-dom": "^16.12.0",
  "react-scripts": "3.4.0",
  "yup": "^0.28.1" /* Make sure you see this line */
}

Now let's import it into our project.

// Top of LoginForm.jsx
import React from 'react';
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as Yup from 'yup';  /* Add this line to */

class LoginForm extends React.Component {
  ...
}

The new Yup object we've imported has the ability to create JSON schemas via the object method. Let's add this code just above our LoginForm:

import * as Yup from 'yup';

/* Add this new schema */
const loginFormSchema = Yup.object().shape({
  email: Yup.string()
            .email("Email is not in the expected email address standard format")
            .required("Email is missing"),
  password: Yup.string()
            .required("Password is required")
            .min(6, "Password must be 6 characters at minimum")
});

class LoginForm extends React.Component {
  ...
}

We're going to get rid of the validate prop of the Formik component, but we're going to add a validateSchema prop, to use the new schema:

class LoginForm extends React.Component {
  ...
  render() {
    return (
      <div className="container">
        <div className="row justify-content-center">
          <div className="col-lg-6">
            <div className="col-lg-12">
              <Formik
                initialValues={this.initialValues()}
                /* New prop validateSchema */
                validateSchema={loginFormSchema}>
                ...
              </Formik>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

Let's test our form, to confirm it still works properly.

Alt Text

Success! Our LoginForm component now uses Formik for it's <input> fields and Yup for defining the validations.

Conclusion

React is a very useful framework for building interactive websites. However, because React must control all state on the page, we cannot use vanilla DOM elements. To provide a user experience that most customers expect of web forms, we can use Formik to handle common needs, including validation. We refactored our hand-written validate method to use Yup, which allows us to declaratively describe our form.

Top comments (0)