Building complex forms is a common part of software development for both mobile and the web. Many of the apps we’ve worked on at Tacchi have made use of them in one way or another, for example account and profile creation flows, e-commerce checkouts, and posting job advertisements. Forms written in React, however, a powerful JavaScript library for building user interfaces, can be very verbose, cutting into development time that could be spent on other aspects of the app and potentially impairing maintainability.

In this article I’ll be talking about mitigating these challenges through use of the Formik library. With Formik we’ll decrease boilerplate code and increase development speed, leading to happier clients and happier developers. Note that while the example I’ll be working with below is for a web form, Formik can also be used with mobile apps written in React Native.

Forms written in plain React

One of the first things that struck me when I started using React was the amount of boilerplate code required. This was especially true when building forms, which seemed overly verbose and redundant. For example, consider the following form, which I used for a Formik-related knowledge-sharing session we held at Tacchi:

import React, { useState } from "react";

const MyForm = () => {
  const [values, setValues] = useState({ email: "", password: "" });
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const EMAIL_RE = new RegExp(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/, "i");

  const handleSubmit = event => {
    event.preventDefault();
    setIsSubmitting(true);
    setTimeout(() => {
      alert(JSON.stringify(values, null, 2));
      setIsSubmitting(false);
    }, 1000);
  };

  const handleChange = event => {
    setValues({
      ...values,
      [event.target.name]: event.target.value
    });
  };

  const handleBlur = event => {
    let name = event.target.name;

    if (
      name === "email" &&
      event.target.value &&
      !EMAIL_RE.test(event.target.value)
    ) {
      setErrors({
        ...errors,
        email: "Not a valid email"
      });
    } else if (
      name === "password" &&
      event.target.value &&
      event.target.value.length < 8
    ) {
      setErrors({
        ...errors,
        password: "Password must be at least 8 characters"
      });
    } else if (!event.target.value) {
      setErrors({
        ...errors,
        [name]: "Required"
      });
    } else if (event.target.value) {
      const errorToRemove = [name];
      const { [errorToRemove]: removed, ...updatedErrors } = errors;

      setErrors({
        ...updatedErrors
      });
    }
  };

  return (
    <div>
      <h1>Log in</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label>Email</label>
          <input
            type="email"
            name="email"
            onChange={handleChange}
            onBlur={handleBlur}
          />
          {errors.email && <div style={{ color: "red" }}>{errors.email}</div>}
        </div>

        <div>
          <label>Password</label>
          <input
            type="password"
            name="password"
            onChange={handleChange}
            onBlur={handleBlur}
          />
          {errors.password && (
            <div style={{ color: "red" }}>{errors.password}</div>
          )}
        </div>

        <br />

        <button
          type="submit"
          disabled={
            Object.keys(values).length === 0 ||
            Object.keys(errors).length > 0 ||
            isSubmitting
          }
        >
          Submit
        </button>
      </form>
    </div>
  );
};

export default MyForm;

We’re setting the form’s initial values in the first usage of useState. We’re also using state to store error messages and to keep track of whether the form is being submitted. We then have several event handlers to update said values and errors whenever an input changes or a user clicks on an input field and then navigates away from it. Additionally, we have an event handler for form submission, which changes the form’s submitted values into text strings and displays them in an alert box while also preventing the form from being mistakenly double-submitted.

Now while the above is a perfectly usable form, having to write all of the event handlers and validation logic can take a substantial amount of time, particularly for more involved forms. Having less code would be easier to maintain and would speed up development—wins for both Tacchi and our clients. What if I told you there’s a way we could do just that, and without affecting functionality?

Enter Formik

While there are a number of libraries for taking the redundancy out of form building with React, far and away the favorite, in terms of downloads at least, is Formik. As it states in the GitHub repo, Formik helps with handling form state in regard to values, validations, and submission-handling. You make use of Formik by nesting your form within the provided Formik component and then exposing your values, errors, and event handlers through that component’s props.

Additionally, Formik works nicely with various validators, notably Yup, simplifying another part of the process. By using Formik and Yup instead of your own custom event handlers and validations, you can slash the number of lines of code required to get your form working.

On to the meat of this article: Let’s try refactoring the above example code to see how Formik can help simplify it.

First, we nest our form inside a Formik component:

import React, { useState } from "react";
import { Formik } from "formik";

const MyForm = () => {
  const [values, setValues] = useState({ email: "", password: "" });
  // ...
  
  const handleBlur = event => {
    // ...
  }

  return(
    <div>
      <h1>Log in</h1>
      <Formik> 
        {props => (
          <form onSubmit={handleSubmit}>
            ...

The Formik component has an initialValues prop which we can use instead of defining those values in useState:

import React, { useState } from "react";
import { Formik } from "formik";

const MyForm = () => {
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  // ...

  const handleBlur = event => {
    // ...
  }
  
  return(
    <div>
      <h1>Log in</h1>
      <Formik initialValues={{ email: "", password: ""}}>
        {props => (
          <form onSubmit={handleSubmit}>
            ...

We’ve now delegated part of our state management to Formik. Next, let’s get rid of our custom handleChange and handleSubmit event handlers and use Formik’s event handlers instead:

import React, { useState } from "react";
import { Formik } from "formik";

const MyForm = () => {
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const EMAIL_RE = new RegExp(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/, "i");

  const handleBlur = event => {
    // ...
  }

  return (
    <div>
      <h1>Log in</h1>
      <Formik
        initialValues={{ email: "", password: "" }}
      >
        {props => (
          <form onSubmit={props.handleSubmit}>
            <div>
              <label>Email</label>
              <input
                type="email"
                name="email"
                onChange={props.handleChange}
                onBlur={handleBlur}
              />
              {errors.email && (
                <div style={{ color: "red" }}>{errors.email}</div>
              )}
            </div>

            <div>
              <label>Password</label>
              <input
                type="password"
                name="password"
                onChange={props.handleChange}
                onBlur={handleBlur}
              />
              {errors.password && (
                <div style={{ color: "red" }}>
                  {errors.password}
                </div>
              )}
            </div>
              
            <br />
              
            <button
              type="submit"
              disabled={
                Object.keys(props.values).length === 0 ||
                Object.keys(errors).length > 0 ||
                isSubmitting
              }
            >
              Submit
            </button>
            ...

If you’re testing this in a browser, you’ll now notice the Submit button no longer works. Checking the console, we see the following error:

Uncaught (in promise) TypeError: _this.props.onSubmit is not a function.

Formik lets us define our own onSubmit prop. This will be called by Formik’s handleSubmit event handler. Let’s make use of that prop to replicate the original functionality; i.e., displaying the form values in an alert box as a stringified JavaScript object:

  // ...
 
  return (
    <div>
      <h1>Log in</h1>
      <Formik
        initialValues={{ email: "", password: "" }}
        onSubmit={values => {
          alert(JSON.stringify(values, null, 2));
        }}
      >
        {props => (
          <form onSubmit={props.handleSubmit}>
            <div>
              ...

Our form is now working again.

Next, let’s tackle validation. Since I’m using the onBlur event attribute to fire our validations, let’s first replace our handleBlur event handler with that of Formik’s:

import React, { useState } from 'react';
import { Formik } from "formik";

const MyForm = () => {
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  return (
    ...
      
      <form onSubmit={props.handleSubmit}>
        <div>
          <label>Email</label>
          <input
            type="email"
            name="email"
            onChange={props.handleChange}
            onBlur={props.handleBlur}
          />
          ...

This will give us a prop for each named input that we can use to determine when a user has deselected an input (“touched”) so that we can display an error message if a user tabs away from an input without first entering valid data (for example, an incorrectly formatted email address). Before we get to that, however, as all our custom validations were removed when we erased the handleBlur event handler, we can replace and simplify these by using a validator library with Formik.

Validating with Yup

Yup is so well loved by Formik’s creator he’s created a special prop to ease its implementation, validationSchema. We can add Yup by defining a Yup schema object with validation errors for our inputs and passing it to the aforementioned prop. (While there are a great number of options available when defining a Yup schema object, for simplicity’s sake I’ve only replicated validations similar to those of our original form.)

import React, { useState } from "react";
import { Formik } from "formik";
import * as Yup from "yup";

const MyForm = () => {
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const myValidation = Yup.object().shape({
    email: Yup.string()
      .required("Required")
      .email("Not a valid email"),
    password: Yup.string()
      .required("Required")
      .min(8, "Password must be at least 8 characters")
  });

  return (
    <div>
      <h1>Log in</h1>
      <Formik
        initialValues={{ email: "", password: "" }}
        onSubmit={values => {
          alert(JSON.stringify(values, null, 2));
        }}
        validationSchema={myValidation}
      >
        {props => (
          <form onSubmit={props.handleSubmit}>
            ...

We need to check that each input has been touched before we display error messages as otherwise error messages will appear while we are in the middle of inputting a value and tabbing away from one input field will mistakenly display errors for both inputs:

...

  {props => (
    <form onSubmit={props.handleSubmit}>
      <div>
        <label>Email</label>
        <input
          type="email"
          name="email"
          onChange={props.handleChange}
          onBlur={props.handleBlur}
        />
        {props.touched.email && props.errors.email && (
          <div style={{ color: "red" }}>{props.errors.email}</div>
        )}
      </div>

      <div>
        <label>Password</label>
        <input
          type="password"
          name="password"
          onChange={props.handleChange}
          onBlur={props.handleBlur}
        />
        {props.touched.password && props.errors.password && (
          <div style={{ color: "red" }}>{props.errors.password}</div>
        )}
      </div>

      <br />

      <button
        type="submit"
        disabled={
          Object.keys(props.values).length === 0 ||
          Object.keys(props.errors).length > 0 ||
          isSubmitting
        }
      >
        Submit
      </button>
      
      ...

Since we’re now using the Formik component to keep track of our errors, we can remove all references to useState entirely, provided we update onSubmit to also set isSubmitting:

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

const MyForm = () => {
  const myValidation = Yup.object().shape({
    // ...

  return (
    <div>
    <h1>Log in</h1>
    <Formik
      initialValues={{ email: "", password: "" }}
      onSubmit={(values, { setSubmitting }) => {
        alert(JSON.stringify(values, null, 2));
        setSubmitting(false);
      }}
      validationSchema={myValidation}
    >
      ...

      <button
        type="submit"
        disabled={
          Object.keys(props.values).length === 0 ||
          Object.keys(props.errors).length > 0 ||
          props.isSubmitting
        }
      >
        Submit
      </button>
      
      ...

Lastly, let’s make use of Formik’s helper components to further dry up our code. We’re going to be using the following helpers:

  • Field, which receives the appropriate handleChange and handeBlur event handlers based on the input’s name attribute.
  • ErrorMessage, which checks both whether the input has been touched and whether it has any errors.
  • Form, which receives onSubmit.

import React from "react";
import { Formik, Field, ErrorMessage, Form } from "formik";
import * as Yup from "yup";

const MyForm = () => {
  // ...

    {props => (
      <Form>
        <div>
          <label>Email</label>
          <Field type="email" name="email" />
          <ErrorMessage
            name="email"
            component="div"
            style={{ color: "red" }}
          />
        </div>

        <div>
          <label>Password</label>
          <Field type="password" name="password" />
          <ErrorMessage
            name="password"
            component="div"
            style={{ color: "red" }}
          />
        </div>

        ...

And with that, we’ve now reached the end of our little refactoring exercise. We’ve taken a file that was a bit over 100 lines and cut it down to one that’s only about 60—nearly half its original size!—and we’ve done so without sacrificing readability. In fact, it’s even easier to read now, being both shorter and simpler. By keeping our code as simple as possible (but no simpler!), we ensure a higher degree of maintainability with increased development speed.

While the above example is rather simplistic, I hope it’s given you a feel for the advantages Formik can provide. It’s not the right solution for every form-related problem that comes from using React, but it can help lessen much of the pain. If you haven’t already, give Formik a try in your next project.