Form Validation in React

Michael Ries
Code Monkey
Published in
7 min readSep 10, 2017

--

Client-side validation is the process of checking that the values in your form’s input fields conform to certain expectations. Did the user fill out all the required fields? Is that an email address in the email field, or is it just gibberish? Is that comment too long or not long enough? Client-side validation improves the user experience by providing quick feedback, sometimes even before the user clicks ‘submit’. It also helps reduce server load and bandwidth by making fewer round trips.

Last weekend, I added client-side validation to my Dive-Log project. Client-side validation is a common feature of most modern web apps, so I started by doing a Google search for existing packages that I could use. I found react-validation but didn’t like that it was tightly interwoven into the form. I had already written and styled my forms and didn’t want to do them over. Most of the other search results were for other people like me, who decided to roll their own.

One in particular was posted just a few days ago by Hrishi Mittal. His tutorial is well written and easy to follow, but ultimately I don’t like that his method requires that I change how I store the form state. And as he admits, it’s a simple example that is limited to one validation per field and isn’t terribly scalable.

So I wrote my own react validation code, and am sharing it with you.

The Plan

My design goals are as follows:

  • Portability/Reusability. I want to be able to use the same validation code several places in my project, and in future projects.
  • Flexibility. It needs to be able to handle most common validation scenarios, like checking if a field is empty, that it’s numeric, etc.
  • Multiple validations per field. This is a very common requirement.
  • Validations based on more than one field. This is a trickier one, but is surprisingly familiar: Does the first password confirmation field match the password field? Did the user fill out either the phone or the email field?
  • Easily integrated. Please don’t make me redesign my form.

To achieve these, I decided to create a FormValidator object that might look something like this:

class FormValidator {
constructor(validations) {
// validations is an array of form-specific validation rules
this.validations = validations;
}
validate(state) {
// iterate through the validation rules and construct
// a validation object, and return it
return validation;
}
}

To use this object, my form just creates an instance of the FormValidator, passing into an array of validation rules. (We’ll discuss validation rules in the next section)

const validator = new FormValidator([rule1, rule2, rule3]);

Then when the form wants to check if state is valid, it calls validate(state).

const validation = validator.validate(this.state);

The form can then use the validation object to determine if the form can be submitted and what to display. We’ll get to the details of the validation object in a minute or two. But first, how to write the rules?

Validation Rules

Conceptually, a validation rule needs the following things:

  1. What field is being validated.
  2. What function should be invoked to check if it’s valid
  3. What that function should return when the field is valid (usually true or false)
  4. What message should be displayed if the field isn’t valid.

Fortunately, there is the really useful validator package that contains nearly everything we need for item #2. Using validator, we can write

import validator from 'validator'validator.isEmpty('') // returns true
validator.isEmpty('tim@home.com') // returns false
validator.isEmail('tim@home.com') // returns true
validator.isEmail('go away') // return false

Ok, so then why not write a rules for an email field like this:

validator = new FormValidator([
{
field: 'email',
method: validator.isEmpty,
validWhen: false,
message: 'Please provide an email address.'
},
{
field: 'email',
method: validator.isEmail,
validWhen: true,
message: 'That is not a valid email.'
}
]);

That works great! But wait, some of the functions in the validator package take options or require some additional arguments. Suppose I have an age field and I want to use validator.isInt function to check that the value is between 21 and 65? So let’s add args to the rules:

validator = new FormValidator([
...
{
field: 'age',
method: validator.isInt,
args: [{min: 21, max: 65}], // an array of additional arguments
validWhen: true,
message: 'Your age must be an integer between 21 and 65'
}
]

That’s all we need! With these rules and the current value of the form’s state, the FormValidator instance will have everything it needs to check if the form is valid.

The Validation Object

Before we go into detail about how the FormValidator validate method works, we need to think about what the form needs to get back. How do we plan to use the information we get back?

Well the most obvious thing is that we will want to be able to quickly check if the form is valid so that we know if we should submit it or not. So let’s start by requiring that our validation object has an isValid property.

// retrieve the validation object
validation = validator.validate(this.state);
if (validation.isValid) { // if it's valid
// handle form submission here
}

If the form state is not valid, we will want to highlight invalid fields and display error messages. We want to be able to do things like

//add an error class to the div for styling if invalid
<div className={validation.email.isInvalid && 'has-error'}>
... // <input> specifics here
// add the error message below the input field
<span className="help-block">{validation.email.message}</span>
</div>

This means our validation object will need to have an object for each form field, each of which has an isInvalid property and a message property. In our imaginary form with email and age fields, this might be

// let's suppose that state is
state = { email: 'loony tunes', age: 19}
validation = validator.validate(state)// the validation object that would result
{
isValid: false,
email: {
isInvalid: true,
message: 'Please enter a valid email address.'
},
age: {
isInvalid: true,
message: 'Your age must be an integer between 21 and 65'
}
}

The FormValidator Class

Now that we’ve defined our validation rules and the resulting validation object that we want, we can finally create our FormValidator class.

I’ve copied the code below and inserted comments to explain each piece.

import validator from 'validator';class FormValidator {
constructor(validations) {
// validations is an array of rules specific to a form
this.validations = validations;
}
validate(state) {
// start out assuming valid
let validation = this.valid();
// for each validation rule
this.validations.forEach(rule => {

// if the field isn't already marked invalid by an earlier rule
if (!validation[rule.field].isInvalid) {
// determine the field value, the method to invoke and
// optional args from the rule definition

const field_value = state[rule.field].toString();
const args = rule.args || [];
const validation_method = typeof rule.method === 'string' ?
validator[rule.method] :
rule.method
// call the validation_method with the current field value
// as the first argument, any additional arguments, and the
// whole state as a final argument. If the result doesn't
// match the rule.validWhen property, then modify the
// validation object for the field and set the isValid
// field to false
if(validation_method(field_value, ...args, state) != rule.validWhen) {
validation[rule.field] = {
isInvalid: true,
message: rule.message
}
validation.isValid = false;
}
}
});
return validation;
}
// create a validation object for a valid form
valid() {
const validation = {}

this.validations.map(rule => (
validation[rule.field] = { isInvalid: false, message: '' }
));
return { isValid: true, ...validation };
}
}
export default FormValidator;

I threw a curveball in there that I need to point out. I didn’t like that I had to remember to import validator from 'validator' in each of my forms so that I could create rules that used its functions. So I added a feature that if the rule.method property is a string, the validate() function uses the validator function named by rule.method. Otherwise it uses the function you pass.

I have created a demonstration project that uses this code to implement a basic signup form. Among other things, it checks the presence and format of an email field, uses the validator.matches method and a regex to check a phone number, and confirms that two entered passwords match each other. No validation is performed until the user presses the submit button, but then the form is validated with each change to a field.

This is a simple but flexible approach to form validation in react. For a larger project with many forms, you might extract rule definitions into a separate file to facilitate re-use. But for most small projects this is easier and works well.

I feel obligated to point out that client-side validation cannot replace server-side validation for your project. It’s not secure and not everything can be validated by the client — checking if a username has already been taken, for example. And because it is asynchronous, handling errors returned by the server can be tricky. I plan to address this problem in a future article.

I hope you find this post helpful, and I welcome your comments and suggestions for improvement. And if you’re looking for a software developer, drop me a line!

--

--

Michael Ries
Code Monkey

Solving complex problems one simple problem at a time.