React Form: Real Time Validation Using State

We have disabled submit button before all field validation are passed. Great! But this will also cause confusion for our users since there is no hint of what is wrong with the form. We have to let our users know which input is not passing validation, so let's go one step further and add input validation feedback as our user type into our input elements.

Adding field validation method
Adding field validation method

Starting Code

We will continue with the starter code from the previous post, with the email validation solution using regex.

Our goal is to give appropriate error messages as user type into our input fields. We will do two things:

  1. Highlight the field with error using red border around it
  2. Display error text below the field

We'll use React state to create this validation.

Adding Error divs

Let's modify our form to include an empty div below each input fields. These divs have one job to do: displaying validation error messages.

<input
  className="form-control"
  id="name"
  name="email"
  type="text"
  placeholder="Enter email"
  value={this.state.email}
  onChange={this.handleChange}
/>
// this div
<div className="invalid-feedback">{this.state.formErrors.email}</div>

// add this below username input
<div className="invalid-feedback">{this.state.formErrors.username}</div>

// add this below password input
<div className="invalid-feedback">{this.state.formErrors.password}</div>

Using React State To Validate Fields

There are two things we have to keep track of in order to create real time validations: the validity of each field and the error actually encountered by the field. We will create two extra properties in our state object. Let's update the state initialization in our constructor method:

this.state = {
  email: '',
  username: '',
  password: '',
  formErrors: {email: '', username:'', password: ''},
  formValidity: {email: false, username: false, password: false},
  isSubmitDisabled: true,
};

Next, we have to do field validation right after handleChange event. Let's update our handleChange method to have a different setState callback named validateField. Also, let's store our event.target properties into variables using ES6 destructuring assignment to avoid typing event.target repeatedly.

handleChange(event) {
    const {name, value} = event.target
    this.setState({
      [name]: value
    }, function(){ this.validateField(name, value)})
    
  }

Now we're ready to create the validateField function. Let's start by assigning both formErrors and formValidity to constant variables to make assignment easier. We also add the emailTest regular expression that will be used to check email formatting.

validateField(name, value) {
  const fieldValidationErrors = this.state.formErrors
  const validity = this.state.formValidity
  const emailTest = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i
}

Now we will start with the easiest validation logic first — not empty validation. We simply have to set our validity and fieldValidationErrors object property using our value's value. Take a look at the following code:

validateField(name, value) {
  const fieldValidationErrors = this.state.formErrors
  const validity = this.state.formValidity
  const emailTest = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i

  validity[name] = value.length >0
  fieldValidationErrors[name] = validity[name] ? '': `${name} is required and cannot be empty`;

  this.setState({
    formErrors: fieldValidationErrors,
    formValidity: validity,
  }, () => this.canSubmit())
}

After assigning values to our validity and fieldValidationErrors object, update the state formErrors and formValidity using the constants. After that, we will add another callback to the setState method.

Now we need to update our canSubmit method as follows:

canSubmit() {
    if(this.state.formValidity.email && this.state.formValidity.username && this.state.formValidity.password){
      this.setState({isSubmitDisabled: false});
    }
    else {
      this.setState({isSubmitDisabled: true});
    }
  }

This is why we need the formValidity state. We have to keep track of all input fields and make sure all inputs are valid so that we can enable the submit button.

Now that our function is ready, it's time to hightlight our input field red if it has an error. We have to add modify our input fields className attribute as follows:

// a new function

errorClass(error) {
  return(error.length === 0 ? '' : 'is-invalid');
}

// other codes

<input
// update className attribute
  className={`form-control ${this.errorClass(this.state.formErrors.email)}`}
  id="email"
  name="email"
  type="text"
  placeholder="Enter email"
  value={this.state.email}
  onChange={this.handleChange}
/>
// for username
<input
  className={`form-control ${this.errorClass(this.state.formErrors.username)}`}
  id="username"
  name="username"
  type="text"
  placeholder="Enter username"
  value={this.state.username}
  onChange={this.handleChange}
/>

// for password
<input
  className={`form-control ${this.errorClass(this.state.formErrors.password)}`}
  id="password"
  name ="password"
  type="password"
  placeholder="Enter password"
  value={this.state.password}
  onChange={this.handleChange}
/>

Now try to type something into any input field and empty it. You'll see the validation error message displayed and the input box highlighted red. We have accomplished our tasks! 👌

Making Field Validation Specific

We successfully validated empty field, but this is not good enough. Let's make it more advanced by displaying error message specific to the field being validated. We have to check the specifity of our name parameter.

validateField(name, value) {
  const fieldValidationErrors = this.state.formErrors
  const validity = this.state.formValidity
  const emailTest = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i
  const isEmail = name === "email"
  const isPassword = name === "password"

  // ... the rest of the function

}

This way we can know if the field being validated is an email field or a password field. Next, we just have to add validation logic depending on the boolean value of isEmail and isPassword constants. The full validateField method would look like the following:

  validateField(name, value) {
    const fieldValidationErrors = this.state.formErrors
    const validity = this.state.formValidity
    const isEmail = name === "email"
    const isPassword = name === "password"
    const emailTest = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i

    validity[name] = value.length >0
    fieldValidationErrors[name] = validity[name] ? '': `${name} is required and cannot be empty`;

    if(validity[name]) {
      if(isEmail){
        validity[name] = emailTest.test(value);
        fieldValidationErrors[name] = validity[name] ? '' : `${name} should be a valid email address`;
      }
      if(isPassword){
        validity[name] = value.length >= 5;
        fieldValidationErrors[name] = validity[name] ? '': `${name} should be 5 characters or more`;
      }
    }
  
    this.setState({
      formErrors: fieldValidationErrors,
      formValidity: validity,
    }, () => this.canSubmit())
  }

That's all we have to do for now. Go ahead and try if the instant validation feature is implemented by typing into our fields.

Refactoring isSubmitDisabled State

Our form is good, but there is one small refactoring I'd like to do with our isSubmitDisabled and canSubmit method. It works, but it doesn't look very nice in its current state. Let's change isSubmitDisabled state into canSubmit state.

this.state = {
  email: '',
  username: '',
  password: '',
  formErrors: {email: '', username:'', password: ''},
  formValidity: {email: false, username: false, password: false},  
  canSubmit: false, // change this state
};

// other codes
canSubmit() {
  this.setState({canSubmit: this.state.formValidity.email && this.state.formValidity.username && this.state.formValidity.password})
}

// other codes

<button className="btn btn-success btn-block" disabled={!this.state.canSubmit}>Sign up</button>

This looks a lot better now 👍

Conclusion

We have learned how to create our own validation logic and display feedback for our user in real time as they type. Our form is looking pretty good, but there is still room for improvements. For one, we can add password confirmation field and use it for validation user's password input. Let's do it in the next post.

Here is the codepen for the finished tutorial.


Get my weekly newsletter ✉️
Let's explore the exciting world of React together.
No Spam. Unsubscribe anytime