Three ways to build React forms– Part 3

Using Formik to simplify state management

Formik is a form library that’s very flexible, but also has a concise interface that’s quick to learn. In this final part of our series on building React forms, we’ll rebuild the form from parts 1 and 2 with Formik. We’ll discuss some benefits a drawbacks of using Formik, and highlight the alternatives.

To recap: in Part 2 we created a controlled component form and implemented our own validation with useState and useEffects. That form allowed us to take full control of the UX and create simple but fully custom input validation. However, it was much more complex than our example from Part 1 that used default browser validation. If the form were to become more complex we’d need to duplicate a lot of change-handling boilerplate. Also, our validation system in Part 2 was rudimentary. For thorough validation we’d need to write a lot more code. It’s pretty clear how a form library like Formik coupled with a validation library can save us a lot of time.

As with the previous parts, we’re working with Formspree on the backend, but the principles should still apply if you’re using your own form backend.

Building a Formik contact form with validation

In addition to Formik, we’re going to use Yup, a fully featured validation library with a composeable rule API. Formik was built specifically to work with Yup.

First let’s add Formik and Yup to our environment:

    npm i --save formik yup

Now let’s create our form component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
    import React, { useState } from "react";
    import axios from "axios";
    import { Formik, Form, Field, ErrorMessage } from "formik";
    import * as Yup from "yup";
    
    const formSchema = Yup.object().shape({
      email: Yup.string()
        .email("Invalid email")
        .required("Required"),
      message: Yup.string().required("Required")
    });
    
    export default () => {
      /* Server State Handling */
      const [serverState, setServerState] = useState();
      const handleServerResponse = (ok, msg) => {
        setServerState({ok, msg});
      };
      const handleOnSubmit = (values, actions) => {
        axios({
          method: "POST",
          url: "http://formspree.io/YOUR_FORM_ID",
          data: values
        })
          .then(response => {
            actions.setSubmitting(false);
            actions.resetForm();
            handleServerResponse(true, "Thanks!");
          })
          .catch(error => {
            actions.setSubmitting(false);
            handleServerResponse(false, error.response.data.error);
          });
      };
      return (
        <div>
          <h1>Contact Us</h1>
          <Formik
            initialValues={{ email: "", message: "" }}
            onSubmit={handleOnSubmit}
            validationSchema={formSchema}
          >
            {({ isSubmitting }) => (
              <Form id="fs-frm" noValidate>
                <label htmlFor="email">Email:</label>
                <Field id="email" type="email" name="email" />
                <ErrorMessage name="email" className="errorMsg" component="p" />
                <label htmlFor="message">Message:</label>
                <Field id="message" name="message" component="textarea" />
                <ErrorMessage name="message" className="errorMsg" component="p" />
                <button type="submit" disabled={isSubmitting}>
                  Submit
                </button>
                {serverState && (
                  <p className={!serverState.ok ? "errorMsg" : ""}>
                    {serverState.msg}
                  </p>
                )}
              </Form>
            )}
          </Formik>
        </div>
      );
    };

A few notes:

  1. Formik takes care of the isSubmitting state. However, we still need some way to track and display the response from the server. We’ve done that by defining serverState and handleServerResponse on line 16. This is similar to, though a bit simpler than, our server state handling from Part 1.
  2. The handleOnSubmit function uses Formik provided actions for resetForm and setSubmitting on lines 26 and 31. Previously we needed to managing that state ourselves.
  3. On line 6 we’ve defined two validation rules using Yup that match the validation we performed in Part 2. The Yup API allows us to chain rules together, making our rule definitions relatively easy to read. This is a simple ruleset. There are many more validation rules that we can take advantage of listed in the Yup docs.
  4. The render function takes advantage of the and components to automatically handle state updates from Formik. Otherwise the form is identical to the HTML form we created in Part 2.

Benefits, Drawbacks and Alternatives

As we’ve mentioned a few times, the main benefits of Formik and Yup are that they save us time. Much of the state and rule definitions are done for us. Using Formik’s Field and ErrorMessage components give us a concise way to build the form HTML.

The downside of Formik is that, like any library, it makes assumptions about how the form should work. Luckily, Formik is very flexible in its core so you can override these assumptions. However, this trades writing custom code for customization code. Customization code is typically not generalizable, and requires us to learn how the underlying system works.

In the case of Formik the main things we need to be aware of are:

  • Validation occurs for each input on blur, rather than on submit. Formik calls this “touched”. This is a departure from the way we implemented validation in Parts 1 and 2. In Part 1, the default browser validation was only performed after first submit. In Part 2 we emulated that behavior by waiting for the submit, and then validated interactively on each keystroke.
  • If you depart from the ErrorMessage components, you must check for both touched and error states. It may be useful to define a helper function like: const hasError = field => touched[field] && errors[field]
  • Formik submission follows a presubmitvalidatesubmit flow. You can override validation with your own custom logic if you want to avoid Yup. You can also take control of Formik’s state by overriding the handleChange, handleBlur, or handleSubmit functions.

Overall Formik doesn’t add much process or conceptual baggage. It’s great for simple forms, but also shines with forms that use complex validation logic, custom components, and long forms that require performance optimization. (See FastField)

The main alternative to Formik is ReactFinalForm. This is a fully featured form library with a very similar API. The main difference is that RFF has a more comprehensive state system, and exposes more built-in functionality to customization. For example, RFF tracks the active field and allows you to determine what events should should trigger validation (such as onblur or onchange) or even when to re-render. In addition, RFF has built-in functionality for multi-page “wizard” style forms. If you are building big complex forms, definitely take a look at RFF.

That’s a wrap for this series on Three ways to build React forms. Thanks for your attention. If you have any questions or comments, or want to let us know about a form library we missed, please reach out below.


Got Feedback?