Nick Kinlen

Front-end Developer | NYC

Building React Forms with Formik, Yup, React-Bootstrap with a Minimal Amount of Pain and Suffering

A few days ago I applied for a front-end dev job and heard back from the company. They followed up with an email and wanted me to complete a React coding challenge on HackerRank as part of the interview process. The test consisted of only two question and pertained to React.
Sounds easy enough, right?

Enter stage right: form validation in React

The test was timed and only allowed for about 20 minutes of time to fix a semi-built form provided and to add form validation for name, email, phone number, and url input forms. After being tripped up by this task and running out of time I decided it was time to look into form validation libraries for building and validating forms with React.
I decided to learn how to use Formik and Yup to validate forms.
In this article I will show you how to build a simple and straightforward form with Formik and Yup used for the functionality and validation, and React-Bootstrap used for the UI.
This article assumes the reader is familiar with React, create-react-app, and npm. This article will not go through the ins and outs of building a basic form or styling, the focus will be on form validation with Formik and Yup.

Building the React-Bootstrap form

First let's install React-Bootstrap and Styled-Components with npm:
npm install react-bootstrap bootstrapnpm install react-bootstrap bootstrap
npm i styled-components
After installing React-Bootstrap and including the stylesheet in our
index.html
file, let's build a basic form using React-Bootstrap that includes a name, email, phone, and url input:
import React from 'react';
import styled from 'styled-components';
import { Form, Button } from 'react-bootstrap';

// Styled-components styles
const CONTAINER = styled.div`
  background: #F7F9FA;
  height: auto;
  width: 90%;
  margin: 5em auto;
  color: snow;
  -webkit-box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.4);
  -moz-box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.4);
  box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.4);

  @media(min-width: 786px) {
    width: 60%;
  }

  label {
    color: #24B9B6;
    font-size: 1.2em;
    font-weight: 400;
  }

  h1 {
    color: #24B9B6;
    padding-top: .5em;
  }

  .form-group {
    margin-bottom: 2.5em;
  }
`;

const MYFORM = styled(Form)`
  width: 90%;
  text-align: left;
  padding-top: 2em;
  padding-bottom: 2em;

  @media(min-width: 786px) {
    width: 50%;
  }
`;

const BUTTON = styled(Button)`
  background: #1863AB;
  border: none;
  font-size: 1.2em;
  font-weight: 400;

  &:hover {
    background: #1D3461;
  }
`;

const BasicForm = () => {
  return (
    <CONTAINER>
      <MYFORM className="mx-auto">
        <Form.Group controlId="formName">
          <Form.Label>Name :</Form.Label>
          <Form.Control
            type="text"
            name="name"
            placeholder="Full Name"
            />
        </Form.Group>
        <Form.Group controlId="formEmail">
          <Form.Label>Email :</Form.Label>
          <Form.Control
            type="text"
            name="email"
            placeholder="Email"
          />
        </Form.Group>
        <Form.Group controlId="formPhone">
          <Form.Label>Phone :</Form.Label>
          <Form.Control
            type="text"
            name="phone"
            placeholder="Phone"
            />
        </Form.Group>
        <Form.Group controlId="formBlog">
          <Form.Label>Blog :</Form.Label>
          <Form.Control
            type="text"
            name="blog"
            placeholder="Blog URL"
            />
        </Form.Group>
        <BUTTON variant="primary" type="submit">
          Submit
        </BUTTON>
      </MYFORM>
    </CONTAINER>
  );
}


export default BasicForm;
Here's what our form looks like :
Now that our basic contact form is built let's install Formik and Yup and start building the validation features:
npm i formik
npm i yup
Formik gives us access to a wealth of form related functionality such as storing the values of input forms, error handling, onSubmit functionality, and validation. Yup is just an "object schema validator" that pairs nicely with Formik and will be used to validate the input fields in the form.
After importing Formik and Yup in our React component the first thing we need to do is wrap the form itself inside a
<Formik>
tag. After wrapping the form within the
<Formik>
tag we will set the initial values of the form's inputs using Formik's
initialValues
prop.
This consists of setting an object containing all the initial starting values for our form, like this:
//Sets initial values for form inputs
<Formik initialValues={{ name:"", email:"", phone:"", blog:""}}>
After the initial values are all set we need to call a function within the
Formik
tag. Formik's documentation lists a long list of available helpers. In our case we'll be using
values
,
errors
,
touched
,
handleChange
,
handleBlur
,
handleSubmit
, &
isSubmitting
.
After initializing our our form and calling the callback function with the necessary parameters our form looks like this:
const BasicForm = () => {
  return (
    <CONTAINER>
      //Sets initial values for form inputs
      <Formik initialValues={{ name:"", email:"", phone:"", blog:""}}>
        {/* Callback function containing Formik state and helpers that handle common form actions */}
      {( {values,
          errors,
          touched,
          handleChange,
          handleBlur,
          handleSubmit,
          isSubmitting }) => (
        <MYFORM className="mx-auto">
          <Form.Group controlId="formName">
            <Form.Label>Name :</Form.Label>
            <Form.Control
              type="text"
              name="name"
              placeholder="Full Name"
              />
          </Form.Group>
          <Form.Group controlId="formEmail">
            <Form.Label>Email :</Form.Label>
            <Form.Control
              type="text"
              name="email"
              placeholder="Email"
            />
          </Form.Group>
          <Form.Group controlId="formPhone">
            <Form.Label>Phone :</Form.Label>
            <Form.Control
              type="text"
              name="phone"
              placeholder="Phone"
              />
          </Form.Group>
          <Form.Group controlId="formBlog">
            <Form.Label>Blog :</Form.Label>
            <Form.Control
              type="text"
              name="blog"
              placeholder="Blog URL"
              />
          </Form.Group>
          <BUTTON variant="primary" type="submit">
            Submit
          </BUTTON>
        </MYFORM>
      )}
      </Formik>
    </CONTAINER>
  );
}


export default BasicForm;
Now that our values are initialized and our callback function contains the proper parameters let's update the field forms and connect them to Formik by adding
onChange
,
onBlur
, and
value
to our form properties:
const BasicForm = () => {
  return (
    <CONTAINER>
      //Sets initial values for form inputs
      <Formik initialValues={{ name:"", email:"", phone:"", blog:""}}>
        {/* Callback function containing Formik state and helpers that handle common form actions */}
      {( {values,
          errors,
          touched,
          handleChange,
          handleBlur,
          handleSubmit,
          isSubmitting }) => (
        <MYFORM className="mx-auto">
          {console.log(values)}
          <Form.Group controlId="formName">
            <Form.Label>Name :</Form.Label>
            <Form.Control
              type="text"
              /* This name property is used to access the value of the form element via values.nameOfElement */
              name="name"
              placeholder="Full Name"
              /* Set onChange to handleChange */
              onChange={handleChange}
              /* Set onBlur to handleBlur */
              onBlur={handleBlur}
              /* Store the value of this input in values.name, make sure this is named the same as the name property on the form element */
              value={values.name}
              />
          </Form.Group>
          <Form.Group controlId="formEmail">
            <Form.Label>Email :</Form.Label>
            <Form.Control
              type="text"
              name="email"
              placeholder="Email"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.email}
            />
          </Form.Group>
          <Form.Group controlId="formPhone">
            <Form.Label>Phone :</Form.Label>
            <Form.Control
              type="text"
              name="phone"
              placeholder="Phone"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.phone}
              />
          </Form.Group>
          <Form.Group controlId="formBlog">
            <Form.Label>Blog :</Form.Label>
            <Form.Control
              type="text"
              name="blog"
              placeholder="Blog URL"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.blog}
              />
          </Form.Group>
          <BUTTON variant="primary" type="submit">
            Submit
          </BUTTON>
        </MYFORM>
      )}
      </Formik>
    </CONTAINER>
  );
}


export default BasicForm;
Notice that we can
console.log(values)
and this will log out all the values in our form.
After filling out the form inputs the following is being logged to the console:

Creating a Yup schema to validate form input

Our React-Bootstrap form is now hooked up to Formik and Formik is handling the input data for us. The next step is to add validation to our form, this is where Yup comes into play.
Using Yup we can create an object schema that tells our form what shape our data should have and the requirements we want each input field to have when being validated (i.e. emails having to be in name@email.com format or names being required to contain at least two letters).
In order to use Yup we will create a
validationSchema
variable and set it equal to a function calling Yup's
Object
and
Shape
methods.
Shape
will contain an object that contains the form input names (name, email, phone, blog). This will allow us to use Yup to set validation rules for our inputted elements and looks like this :
// Schema for yup
const validationSchema = Yup.object().shape({
  name: Yup.string(),
  email: Yup.string(),
  phone: Yup.string(),
  blog: Yup.string()
});
validationSchema contains a property for each of our form elements and
Yup.string()
tells the form that these elements must be of type
string
. Yup offers many different functions for validating form fields.
For the sake of this form we will be using
min
,
max
,
required
,
email
,
matches
, and
url
. Check out the Yup documentation to view more available functions.
min
and
max
allow us to set minimum and maximum lengths for inputted text,
required
allows us to make a field mandatory,
email
requires the input to be in valid email form (i.e. name@email.com), and
url
requires the input to be a valid URL (i.e. http://www.website.com).
Matches
allows us to pass a variable containing a regular expression and check that the inputted form data matches it. We will use
matches
and a regex to check the format of the phone form data.
All of these validation functions allow us to pass an error message as the second parameter.
The complete validationSchema object looks like this:
// RegEx for phone number validation
const phoneRegExp = /^(\+?\d{0,4})?\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{4}\)?)?$/

// Schema for yup
const validationSchema = Yup.object().shape({
  name: Yup.string()
  .min(2, "*Names must have at least 2 characters")
  .max(100, "*Names can't be longer than 100 characters")
  .required("*Name is required"),
  email: Yup.string()
  .email("*Must be a valid email address")
  .max(100, "*Email must be less than 100 characters")
  .required("*Email is required"),
  phone: Yup.string()
  .matches(phoneRegExp, "*Phone number is not valid")
  .required("*Phone number required"),
  blog: Yup.string()
  .url("*Must enter URL in http://www.example.com format")
  .required("*URL required")
});

Hooking up the schema to our Formik form

Formik provides our form with a validationSchema prop and we can use our Yup validationSchema variable to pass our schema to our form like this:
<Formik 
      initialValues={{ name:"", email:"", phone:"", blog:""}}
      // Hooks up our validationSchema to Formik 
      validationSchema={validationSchema}
>

Applying an error class and turning the input box red when there is an error

Now the form is set up to perform validation using the
validationSchema
schema. If we want to turn the form input box red when the user inputs data that doesn't pass our validation tests defined by
validationSchema
, we can add a CSS class to do that and use a ternary operator to test if there is an error and apply the class in the event there is an error :
<Form.Control
   type="text"
   /* This name property is used to access the value of the form element via values.nameOfElement */
   name="name"
   placeholder="Full Name"
   /* Set onChange to handleChange */
   onChange={handleChange}
   /* Set onBlur to handleBlur */
   onBlur={handleBlur}
   /* Store the value of this input in values.name, make sure this is named the same as the name property on the form element */
   value={values.name}
   /* Check if the name field (this field) has been touched and if there is an error, if so add the .error class styles defined in the CSS (make the input box red) */
   className={touched.name && errors.name ? "error" : null}
/>
The
touched
property is one of the parameters called in our
Formik 
callback function and tests if that particular element has been clicked on by the user.
{touched.name && errors.name ? "error" : null}
tests if the name input field has been clicked by the user and if there is an error validating the inputted data. If there is an error we apply the class of error to the element and in our CSS we change the input box's border to red.
This ternary operator will apply this newly added CSS class that turns the input box red :
.error {
    border: 2px solid #FF6565;
  }
Now when a user enters text that doesn't pass our validation tests the input box turns red :

Adding an error message explaining the error

Earlier on in our
validationSchema
when we used Yup to set rules for validation we included error messages to display explaining the input error to the user. How do we go about displaying the error message under the form element it pertains to?
We can use the
touch
object and the
errors
object provided to our form by Formik in the callback function along with a ternary operator to display the appropriate error message:
{touched.name && errors.name ? (
  <div className="error-message">{errors.name}</div>
): null}
This ternary operator will apply this newly added CSS class that handles the styling of the error message :
.error-message {
    color: #FF6565;
    padding: .5em .2em;
    height: 1em;
    position: absolute;
    font-size: .8em;
  }
By adding a ternary operator with the appropriate tests to each form input we can add the correct error message from
validateSchema
for each form input.
The full form and schema now look like this :
// RegEx for phone number validation
const phoneRegExp = /^(\+?\d{0,4})?\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{4}\)?)?$/

// Schema for yup
const validationSchema = Yup.object().shape({
  name: Yup.string()
  .min(2, "*Names must have at least 2 characters")
  .max(100, "*Names can't be longer than 100 characters")
  .required("*Name is required"),
  email: Yup.string()
  .email("*Must be a valid email address")
  .max(100, "*Email must be less than 100 characters")
  .required("*Email is required"),
  phone: Yup.string()
  .matches(phoneRegExp, "*Phone number is not valid")
  .required("*Phone number required"),
  blog: Yup.string()
  .url("*Must enter URL in http://www.example.com format")
  .required("*URL required")
});

const BasicForm = () => {
  return (
    <CONTAINER>
      //Sets initial values for form inputs
      <Formik 
      initialValues={{ name:"", email:"", phone:"", blog:""}}
      // Hooks up our validationSchema to Formik 
      validationSchema={validationSchema}
      >
        {/* Callback function containing Formik state and helpers that handle common form actions */}
      {( {values,
          errors,
          touched,
          handleChange,
          handleBlur,
          handleSubmit,
          isSubmitting }) => (
        <MYFORM className="mx-auto">
          {console.log(values)}
          <Form.Group controlId="formName">
            <Form.Label>Name :</Form.Label>
            <Form.Control
              type="text"
              /* This name property is used to access the value of the form element via values.nameOfElement */
              name="name"
              placeholder="Full Name"
              /* Set onChange to handleChange */
              onChange={handleChange}
              /* Set onBlur to handleBlur */
              onBlur={handleBlur}
              /* Store the value of this input in values.name, make sure this is named the same as the name property on the form element */
              value={values.name}
              /* Check if the name field (this field) has been touched and if there is an error, if so add the .error class styles defined in the CSS (make the input box red) */
              className={touched.name && errors.name ? "error" : null}
              />
              {/* Applies the proper error message from validateSchema when the user has clicked the element and there is an error, also applies the .error-message CSS class for styling */}
              {touched.name && errors.name ? (
                <div className="error-message">{errors.name}</div>
              ): null}
          </Form.Group>
          <Form.Group controlId="formEmail">
            <Form.Label>Email :</Form.Label>
            <Form.Control
              type="text"
              name="email"
              placeholder="Email"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.email}
              className={touched.email && errors.email ? "error" : null}
            />
            {touched.email && errors.email ? (
                <div className="error-message">{errors.email}</div>
              ): null}
          </Form.Group>
          <Form.Group controlId="formPhone">
            <Form.Label>Phone :</Form.Label>
            <Form.Control
              type="text"
              name="phone"
              placeholder="Phone"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.phone}
              className={touched.phone && errors.phone ? "error" : null}
              />
              {touched.phone && errors.phone ? (
                <div className="error-message">{errors.phone}</div>
              ): null}
          </Form.Group>
          <Form.Group controlId="formBlog">
            <Form.Label>Blog :</Form.Label>
            <Form.Control
              type="text"
              name="blog"
              placeholder="Blog URL"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.blog}
              className={touched.blog && errors.blog ? "error" : null}
              />
              {touched.blog && errors.blog ? (
                <div className="error-message">{errors.blog}</div>
              ): null}
          </Form.Group>
          <BUTTON variant="primary" type="submit">
            Submit
          </BUTTON>
        </MYFORM>
      )}
      </Formik>
    </CONTAINER>
  );
}


export default BasicForm;
Now when a user inputs data that doesn't pass our validation test the error message we defined earlier in the schema is displayed.
The end result looks like this :

Handling form submission with Formik

Last but not least our form needs to submit the form when the user clicks the submit button. Formik provides us with an
onSubmit
prop that takes care of this. We pass the
onSubmit
prop the values and also
setSubmitting 
and
resetForm
.
setSubmitting
will be set to true when the user is submitting the form and
resetForm
will be called after the form is submitted to clear the form post submission.
Our Formik tag, with an
onSubmit
function now looks like this:
<Formik
        initialValues={{ name:"", email:"", phone:"", blog:""}}
        validationSchema={validationSchema}
        onSubmit={(values, {setSubmitting, resetForm}) => {
            // When button submits form and form is in the process of submitting, submit button is disabled
            setSubmitting(true);
            
            // Resets form after submission is complete
            resetForm();

            // Sets setSubmitting to false after form is reset
            setSubmitting(false);
        }}
      >
In order to call this
onSubmit
function our form has to have an
onSubmit
prop set to
handleSubmit
:
<MYFORM onSubmit={handleSubmit} className="mx-auto">
In addition to handling the submission of the form we can also disable the submit button while the form is being submitted so that after the user clicks submit the submit button is disabled while the form is in the process of submitting.
We do this by adding a disabled property to the button and setting it equal to
isSubmitting
:
<BUTTON variant="primary" type="submit" disabled={isSubmitting}>
   Submit
</BUTTON>
Now the form submits when the user clicks the submit button, while the form is being submitted the button is inactive, and after submission is complete the form resets and clears the fields.

Simulating submission and viewing what the form is submitting

In a full stack application this is when the form would be submitted via a POST request. For the sake of viewing the data our form is submitting in this database-less example we can
alert
the values when the user submits the form.
Add a
setTimeout
function that has a .5 second delay and contains an
alert
with a
JSON.stringify
function that prints out the form data when the form is submitted.
We can also move the
resetForm
and
setSubmitting
functions inside this
setTimeout
function so they are called after a .5 second delay:
// Simulate submitting to database, shows us values submitted, resets form
          setTimeout(() => {
            alert(JSON.stringify(values, null, 2));
            resetForm();
            setSubmitting(false);
          }, 500);
Our contact form is now complete.
The component, in it's entirety, looks like this:
import React from 'react';
import styled from 'styled-components';
import { Form, Button } from 'react-bootstrap';
import { Formik, ErrorMessage } from 'formik';
import * as Yup from 'yup';

const CONTAINER = styled.div`
  background: #F7F9FA;
  height: auto;
  width: 90%;
  margin: 5em auto;
  color: snow;
  -webkit-box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.4);
  -moz-box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.4);
  box-shadow: 5px 5px 5px 0px rgba(0, 0, 0, 0.4);

  @media(min-width: 786px) {
    width: 60%;
  }

  label {
    color: #24B9B6;
    font-size: 1.2em;
    font-weight: 400;
  }

  h1 {
    color: #24B9B6;
    padding-top: .5em;
  }

  .form-group {
    margin-bottom: 2.5em;
  }

  .error {
    border: 2px solid #FF6565;
  }

  .error-message {
    color: #FF6565;
    padding: .5em .2em;
    height: 1em;
    position: absolute;
    font-size: .8em;
  }
`;

const MYFORM = styled(Form)`
  width: 90%;
  text-align: left;
  padding-top: 2em;
  padding-bottom: 2em;

  @media(min-width: 786px) {
    width: 50%;
  }
`;

const BUTTON = styled(Button)`
  background: #1863AB;
  border: none;
  font-size: 1.2em;
  font-weight: 400;

  &:hover {
    background: #1D3461;
  }
`;

// RegEx for phone number validation
const phoneRegExp = /^(\+?\d{0,4})?\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{4}\)?)?$/

// Schema for yup
const validationSchema = Yup.object().shape({
  name: Yup.string()
  .min(2, "*Names must have at least 2 characters")
  .max(100, "*Names can't be longer than 100 characters")
  .required("*Name is required"),
  email: Yup.string()
  .email("*Must be a valid email address")
  .max(100, "*Email must be less than 100 characters")
  .required("*Email is required"),
  phone: Yup.string()
  .matches(phoneRegExp, "*Phone number is not valid")
  .required("*Phone number required"),
  blog: Yup.string()
  .url("*Must enter URL in http://www.example.com format")
  .required("*URL required")
});

const BasicForm = () => {
  return (
    <CONTAINER>
      //Sets initial values for form inputs
      <Formik
        initialValues={{ name:"", email:"", phone:"", blog:""}}
        validationSchema={validationSchema}
        onSubmit={(values, {setSubmitting, resetForm}) => {
            // When button submits form and form is in the process of submitting, submit button is disabled
            setSubmitting(true);

            // Simulate submitting to database, shows us values submitted, resets form
          setTimeout(() => {
            alert(JSON.stringify(values, null, 2));
            resetForm();
            setSubmitting(false);
          }, 500);
        }}
      >
        {/* Callback function containing Formik state and helpers that handle common form actions */}
      {( {values,
          errors,
          touched,
          handleChange,
          handleBlur,
          handleSubmit,
          isSubmitting }) => (
        <MYFORM onSubmit={handleSubmit} className="mx-auto">
          {console.log(values)}
          <Form.Group controlId="formName">
            <Form.Label>Name :</Form.Label>
            <Form.Control
              type="text"
              /* This name property is used to access the value of the form element via values.nameOfElement */
              name="name"
              placeholder="Full Name"
              /* Set onChange to handleChange */
              onChange={handleChange}
              /* Set onBlur to handleBlur */
              onBlur={handleBlur}
              /* Store the value of this input in values.name, make sure this is named the same as the name property on the form element */
              value={values.name}
              /* Check if the name field (this field) has been touched and if there is an error, if so add the .error class styles defined in the CSS (make the input box red) */
              className={touched.name && errors.name ? "error" : null}
              />
              {/* Applies the proper error message from validateSchema when the user has clicked the element and there is an error, also applies the .error-message CSS class for styling */}
              {touched.name && errors.name ? (
                <div className="error-message">{errors.name}</div>
              ): null}
          </Form.Group>
          <Form.Group controlId="formEmail">
            <Form.Label>Email :</Form.Label>
            <Form.Control
              type="text"
              name="email"
              placeholder="Email"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.email}
              className={touched.email && errors.email ? "error" : null}
            />
            {touched.email && errors.email ? (
                <div className="error-message">{errors.email}</div>
              ): null}
          </Form.Group>
          <Form.Group controlId="formPhone">
            <Form.Label>Phone :</Form.Label>
            <Form.Control
              type="text"
              name="phone"
              placeholder="Phone"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.phone}
              className={touched.phone && errors.phone ? "error" : null}
              />
              {touched.phone && errors.phone ? (
                <div className="error-message">{errors.phone}</div>
              ): null}
          </Form.Group>
          <Form.Group controlId="formBlog">
            <Form.Label>Blog :</Form.Label>
            <Form.Control
              type="text"
              name="blog"
              placeholder="Blog URL"
              onChange={handleChange}
              onBlur={handleBlur}
              value={values.blog}
              className={touched.blog && errors.blog ? "error" : null}
              />
              {touched.blog && errors.blog ? (
                <div className="error-message">{errors.blog}</div>
              ): null}
          </Form.Group>
          <BUTTON variant="primary" type="submit" disabled={isSubmitting}>
            Submit
          </BUTTON>
        </MYFORM>
      )}
      </Formik>
    </CONTAINER>
  );
}


export default BasicForm;

Viewing the example form online

You can view the form online here or check out the code in the Github Repo for this project.

We have now successfully built a form with validation features in React with a minimal amount of pain and suffering.

Tags

More by Nick Kinlen

Topics of interest