Validating a form in React

Much has been written about validating forms in React and hopefully this article will help React developers see clearer.

Here’s what we’re going to do in this article:

  • Build a form with React
  • Validate a form manually, without the help of any external libraries
  • Validate the same form using the helper library Formik
  • Compare the pros and cons of both solutions

The objectives are to understand how forms are built in React, avoid common pitfalls and offer best practices.

If you want to skip the reading, here is the code, here is a running example and here is a codesanbox:

Validating a form in React

Building the form

To create the form, we’re going to create what’s called a controlled component: the form is going to have a state which will contain what the user types in the fields. The form will contain a first name, last name, an age and an email. The first and last name can only contain letters, dashes and a space, and must be at least three characters long. The age must be between 18 and 99, and the email must be valid. The validation rules don’t matter that much, for instance it doesn’t make sense to enforce a minimum length on a name, so don’t focus on that, the idea is to show how to have more than one constraint on a field.

A screenshot of the form we are building
A screenshot of the form we are building

The form must also be able to take in values, so it can be used for both a create and an edit flow. Since we want to use the same form component for both types of validation, the state and the validation logic will be lifted up to a parent component responsible for executing the validation. Each validation component will received as props a set of initial form values and a function to validate each field. So if we look at the app we are going to build, it’s going to look something like this:

A diagram representing the hierarchy of the React components
A diagram representing the hierarchy of the React components
  • App component: holds the initial values for the form, and a function used to validate each field.
  • Validate manually: contains the state for the form, the logic for onBlur/onChange/onSubmit events and invokes validation logic when appropriate.
  • Validate with Formik: contains the state for the form, the logic for onBlur/onChange/onSubmit events and uses Formik to validate the form.
  • Form: a simple functional component that displays input fields.

Let’s get started with the App component.

The validate object is a key value pair where the key is the name of the field and the value is a function that returns a string error, or null if the field is valid. The initialValues object is to populate the form with initial values if so desired.

We’re going to skip the validation for now and look at the Form component. This is the same component that will be used regardless of the type of validation we will use.

The Form component takes in the following props:

  • errors: a key value pair object where the key is the field, and the value is the error detected for that field. If there is no error on the field, the key will not be in the object.
  • touched: a key value pair object where the key is the field, and the value is true if the field has been modified. The key is present only if the user has touched the field (modified its value or triggered an onBlur event). This allows us to know whether or not to display an error.
  • values: a key value pair where the key is the name of the field, and the value is the value of the field (either initial value, or what the user entered)
  • handleSubmit: a function passed to the handleSubmit prop of the form tag. This is the logic that will be executed when the user clicks on the Submit button.
  • handleChange: a function passed to the handleChange prop of the input tags. This is the logic that will save what the user enters in the field.
  • handleBlur: a function passed to the handleBlur prop of the input. This is the logic that will validate the field value.

Essentially, each field will look like this:

<div className="form-group">
<label htmlFor="first-name-input">
First Name *
<input
type="text"
className="form-control"
id="first-name-input"
placeholder="Enter first name"
value={values.firstName}
onChange={handleChange}
onBlur={handleBlur}
name="firstName"
required
/>
{touched.firstName && errors.firstName}
</label>
</div>

Notice how the error is only displayed if the field has been touched.

The form component can be called by any of the validation components we’re going to create. The first one is the one that runs the manual validation.

It is a bit verbose and requires implementing the handleBlur, handleSubmit and handleChange events. The handleChange is the most straightforward, for each event, just update the values state, and set the corresponding touched value to true. The handleBlur event is a little more complicated. The event is triggered after the user leaves the field, which means the value could be valid or not, so the first thing we’ll do is remove the prior error if any. Then we validate the field, and update the errors object. If the field is valid, the property is not added, and if there is an error, the field is updated accordingly.

Finally, the handleSubmit event is the most complicated. We need to validate the form whenever the submit button is clicked even if no value has been touched, because we want to validate the initial values no matter what, and therefore also update the value of the touched values. Then we only allow the submission if there are no errors, if all the fields were touched and every touched field is true. Only then can the form be submitted.

Formik is a convenience library that makes this entire process a little more streamlined.

The same form can be validated with exactly the same rules. Formik does bring some convenience though. For instance, the handleSubmit is only called if the form is valid, so there’s no need to code that condition, the errors and touched objects as well as the handleChange and handleBlur event handlers are all handled automatically, so no convoluted logic to keep those objects in sync. It also becomes very easy to change the validation from onBlur to onChange, and add form level validation (check if two fields are valid depending on their respective values). Finally, Formik provides a few components for graceful error display, and field validation using Yup which is not used in this example so make sure to look at the documentation.

While it’s not overly complicated to validate a form from scratch in React, a lot of things can go wrong, and it seems like a lot of effort for something relatively simple. Formik takes a lot of leg work out of it, and its api helps designing stateless components for the forms. Using Yup also facilitates validation, for example our validation uses a long regex for the email (the one used by email type input fields) and trims string fields before testing them for length to avoid leading and trailing spaces. Yup makes this a whole lot simpler by providing a validation schema and making long if/else functions a thing of the past. In our example, we could have used something like this:

const SignupSchema = Yup.object().shape({
firstName: Yup.string()
.min(3, 'First name needs to be at least three characters')
.max(70, 'Too Long!')
.required('First name is required'),
email: Yup.string()
.email('Please enter a valid email')
.required('Email is required'),
// ...
});

In conclusion, manual validation can be achieved but can reveal itself more complicated than it seems and might not be very future proof. A solution like Formik will make it easier to scale forms as it takes care of even handling and simplifies validation so developers can focus on other things. I am sure there are other good solutions out there, but I would recommend Formik over other popular choices like final-form, react-final-form and redux-form (which I strongly advise against using) and the current trend seems to confirm this.

Please leave some comments if you have any questions, and clap if this is helpful!

Previously Paris and Geneva, currently New York. Can also be found at https://dev.to/arnaud