Elegant Form Validation Using React

Over the past few months, I’ve really been enjoying learning to use React for front-end web development. I love how React encourages you to write clean code by breaking your presentation components into small chunks that are easy to reuse.

Lately, I’ve been working on an approach to add form validation to React components, and I’d like to share what I came up with.

A Sample Problem

My goal was to hide all errors initially, waiting until the user presses “submit” to present any errors. Once the errors were visible, I wanted them to go live and automatically clear as the user changes the input data.

formvalidation

Declaring validation rules

In this example, I’m adding validation to the text fields on a Create Account screen. Looking at the CreateAccount.jsx component below, the first thing you’ll see is the definition of fieldValidations. This is where I define the set of validations that should be performed on each field in the form.

I did this by creating a list of ruleRunners which involves passing a key, a name, and a list of rules. The key is used to identify which value in the component’s state the rule should be run on. The name is a friendly name that will be used to construct an error message when validation fails.

The following line sets up validation for the password field.

ruleRunner("password1", "Password", required, minLength(6)),

This means that this.state.password1 is required to have a value, and that value’s minimum length is six characters. If validation fails, the field should be referred to as “Password” in the resulting error message.

handleFieldChanged()

The next important piece of the CreateAccount component is the handleFieldChanged() function. This is the function that is to be called whenever the text in any of the form fields changes. It starts by creating a copy of this.state and updating it with the value that just changed. It then calls run(), passing in the new state and the list of validation ruleRunners that we declared above.

The run() function performs the validation checks and returns a key-value pair or validation errors. If the value that changed was the password, and it was only five characters, the result would look something like this:

{"password1": "Password must be at least 6 characters"}.

The result of run() is assigned to newState.validationErrors.

Set the component state

Finally, we call this.setState(newState) to commit the change and the updated validation results in our component state.

When the user submits the form data, the validation is already complete. All we have to do is check if validationErrors is an empty object or not. If it’s empty, we can go ahead and perform whatever we need to do to submit the data.

Making errors live

One other thing I’d like to point out is the showErrors flag that exists in the component state. This flag is passed to each of the fields that are rendered as an indicator of whether they should show their corresponding error or not. Initially, all the fields will likely have errors on them because they are empty.

However, since the user hasn’t attempted to submit yet, we don’t want to show these errors. The showErrors flag is defaulted to false, but set to true once the the user clicks submit. From that point on, all errors are visible and updated live as the user changes the form data.


// CreateAccount.jsx

const fieldValidations = [
  ruleRunner("name", "Name", required),
  ruleRunner("emailAddress", "Email Address", required),
  ruleRunner("password1", "Password", required, minLength(6)),
  ruleRunner("password2", "Password Confirmation", mustMatch("password1", "Password"))
];

export default class CreateAccount extends React.Component {
  constructor() {
    // ...
    this.state = {
      showErrors: false,
      validationErrors: {}
    };

    // Run validations on initial state
    this.state.validationErrors = run(this.state, fieldValidations);
  }
  
  handleFieldChanged(field) {
    return (e) => {
      // update() is provided by React Immutability Helpers
      // https://facebook.github.io/react/docs/update.html
      let newState = update(this.state, {
        [field]: {$set: e.target.value}
      });
      newState.validationErrors = run(newState, fieldValidations);
      this.setState(newState);
    };
    
  handleSubmitClicked() {
    this.setState({showErrors: true});
    if($.isEmptyObject(this.state.validationErrors) == false) return null;
    // ... continue submitting data to server
  }

  render() {
    return (
      <div>
        <TextView placeholder="Email address" showError={this.state.showErrors}
                  text={this.props.emailAddress} onFieldChanged={this.handleFieldChanged("emailAddress")}
                  errorText={this.errorFor("emailAddress")} />
        
        // Render Name, Password, Submit button, etc. fields
      <div>
    );
  }
}

ruleRunner

Next, let’s look at the ruleRunner function. ruleRunner is a thunk, or a function that returns a function. We call it to construct a runner function that takes the updated state and runs the specified validations against said state.

We haven’t looked at the implementation of the validation rules yet. For now, you just need to know that they are also thunks. They must return either null (if there was no error) or a function that will be used to construct an error message. That function takes as a parameter the friendly name of the field that was passed in to the original call to ruleRunner() so that a meaningful error message can be made. If a validation check returns a non-null value, the ruleRunner calls the resulting function and then returns a key-value pair of the field key and the error message.

My implementation only allows one error per field to be reported. It would be very simple to tweak the implementation to return an array of error messages instead to support displaying multiple validation errors per field.

run()

The other function that I have defined next to ruleRunner() is run(). This is a very simple function that just calls all the validation rule runners and aggregates their results into a single object. It allows us to have the results of all the fields in one object.

// ruleRunner.js

export const ruleRunner = (field, name, ...validations) => {
  return (state) => {
    for (let v  of validations) {
      let errorMessageFunc = v(state[field], state);
      if (errorMessageFunc) {
        return {[field]: errorMessageFunc(name)};
      }
    }
    return null;
  };
};

export const run = (state, runners) => {
  return runners.reduce((memo, runner) => {
    return Object.assign(memo, runner(state));
  }, {});
};

Implementing rules

Moving one level deeper, the next thing to look at is the implementation of the rules. The rules come in two forms. The simplest is just a function that looks at the value being passed in and returns null or an error message constructor. This is how the required rule works.

The second form is a bit more complicated because it actually constructs a new function that does the validation check. This allows the rules to be reused on various fields with slightly different requirements. For example, the minLength function constructs a rule that checks for a specific length. It could be used somewhere else to check for a different length.

You can see the two different forms of rules when looking at the declaration of field validations that we discussed earlier. Notice that the required rule function is not called in the declaration, but only used as a reference to that function. In contrast, the minLength function is called with a value of six during the declaration. If you find that confusing, you could use a naming convention to help distinguish between the two types. For example, you might name them: requiredRule and minLengthRuleBuilder.

// rules.js
import * as ErrorMessages from './errorMessages.js';

export const required = (text) => {
  if (text) {
    return null;
  } else {
    return ErrorMessages.isRequired;
  }
};

export const mustMatch = (field, fieldName) => {
  return (text, state) => {
    return state[field] == text ? null : ErrorMessages.mustMatch(fieldName);
  };
};

export const minLength = (length) => {
  return (text) => {
    console.log("Checking for length greater than ", length);
    return text.length >= length ? null : ErrorMessages.minLength(length);
  };
};

Building error messages

When a rule determines that a value is invalid, it returns a function that is used to build an appropriate error message. I like to have all my error messages in one place that is easy to find, so I separate them into an errorMessages file. This also makes them easy to reuse if necessary. Alternatively, you could put the implementation of the error message builders right in the rules.

// errorMessages.js

export const isRequired = fieldName => `${fieldName} is required`;

export const mustMatch = otherFieldName => {
  return (fieldName) => `${fieldName} must match ${otherFieldName}`;
};

export const minLength = length => {
  return (fieldName) => `${fieldName} must be at least ${length} characters`;
};

That’s it for the construction and processing of the validation rules.

TextField

The last thing I want to show is my TextField component. As you can see, this component is completely stateless. It displays the value that was passed in. When the user changes the value, it calls the onFieldChanged callback that was also passed in as props. For the error message functionality, it looks at two other props, showError and errorText. If showError is true and errorMessage contains a value, the error message is rendered.

// TextField.jsx

import React from 'react';
import OptionallyDisplayed from './OptionallyDisplayed.jsx';

export default class TextField extends React.Component {

  constructor(props) {
    super(props);
    this.shouldDisplayError = this.shouldDisplayError.bind(this);
  }

  shouldDisplayError() {
    return this.props.showError && this.props.errorText != "";
  }

  render() {
    return (
      <div>
        <input type="text" placeholder={this.props.placeholder}
               value={this.props.text} onChange={this.props.onFieldChanged}  />
        <OptionallyDisplayed display={this.shouldDisplayError()}>
          <div className="validation-error">
            <span className="text">{this.props.errorText}</span>
          </div>
        </OptionallyDisplayed>
      </div>
    );
  }
}

TextField.propTypes = {
  showError: React.PropTypes.bool.isRequired,
  onFieldChanged: React.PropTypes.func.isRequired
};

So that is my approach to form validation of React components. I like it because adding validation to a new form requires only a few lines of code, and the rules are specified in a very declarative way. I also like how the implementation is split up into small manageable pieces, each serving a very specific purpose.

I want to give credit to my coworker Chris Farber, who worked with me to come up with the original implementation of this approach.

Full Working Demo

You can now view a full working demo of this implementation on GitHub:

Elegant Form Validation React – Demo

Conversation
  • Anup Shakya says:

    I tried but it didn’t work.
    Some of the bugs I encountered:
    errorText={this.errorFor(“emailAddress”)} is not define.
    Uncaught ReferenceError : : update is not defined(…)
    handleFieldChanged(field) {
    return (e) => {
    let newState = update(this.state, {
    [field]: {$set: e.target.value}
    });
    newState.validationErrors = run(newState, fieldValidations);
    this.setState(newState);
    }
    };

    If you have working sample, could you share with me ?

    • Jon J says:

      Had some of those issues and also had to figure out what files had to be imported where. Spent a while trying to get this to work but in the end decided it was taking too much time and maybe I should just roll my own. A github sample would go a long way and save some people a lot of time.

      Anyway thanks for your efforts in putting this together!

      • Travis says:

        errorFor() looks like it would be a simple constants file allowing you to keep messaging consistent throughout the app.

        Instead of implementing errorFor() you can just provide the message in text format since all it is doing is setting the property on the TextField for what the error message should be.

  • Laurence James says:

    This is brilliant. Thanks for posting! :)

  • Matt Sloan says:

    Thank you so much!!! Took a little bit of tweaking, but once I got it, it all worked beautifully.

  • Tom Barry says:

    Elegant, indeed. Thank you for this!

  • Faisal says:

    Can we have a sample project for this?

    • Jordan Schaenzle Jordan Schaenzle says:

      I have updated the post with a link to a full working demo.

  • Faradzh says:

    The code organized pretty elegant indeed. I just wonder why you decided to use ‘update’ function to update current state rather than built-in this.setState() function?

    • Jordan Schaenzle Jordan Schaenzle says:

      Thanks! I’m glad you liked it. React’s setState() is really useful for updating top level fields of your state object. However, if your state contains nested objects it’s not great for applying a change to one of those nested items. The update(…) call does not mutate this.state directly. It creates a copy of this.state with the specified mutation applied. Then that entire object is set as the new state by calling setState(…).

  • Faradzh says:

    Another question is why you run validations on initial state inside constructor whereas it gets called only once?

    • Jordan Schaenzle Jordan Schaenzle says:

      Are you suggested that the initial state should be evaluated in a different lifecycle method other than the constructor are are you questioning the need to evaluate the initial state at all?

      • Duller says:

        Why ‘the need to evaluate the initial state at all’ ? Thanks for writing this post!

  • Smith Samuel says:

    I really like this post. Thanks so much. Validation was one of the things that put me off in using React. I tried rolling out my own but I was unsuccessful.

    I will try out this pattern and perhaps it will give me the joy I was looking for.

    Meanwhile, it will be best you have sample project for this writeup in github.

    Thanks

  • Jay says:

    I’m confused about createaccount.jsx line 43. Where does errorFor come from?

  • Shane says:

    After searching and experimenting, this is the most scalable react validation solution I’ve found. Can you please release a full working example to github or package it up as an npm? Some component examples above are partial examples and some are left out completely like the OptionallyDisplayed component. Thanks.

    • Jordan Schaenzle Jordan Schaenzle says:

      I have updated the post with a link to a full working demo.

  • tkrotoff says:

    I’ve created a small (~300 lines of code) lib for React that shared similarities with your approach: https://github.com/tkrotoff/ReactFormWithConstraints

    You don’t need to wrap input inside a component (TextField):

    Should be at least 5 characters long
    !/\d/.test(value)} warning>Should contain numbers
    !/[a-z]/.test(value)} warning>Should contain small letters
    !/[A-Z]/.test(value)} warning>Should contain capital letters
    !/\W/.test(value)} warning>Should contain special characters

    Online demo: https://codepen.io/tkrotoff/pen/BRGdqL
    Bootstrap 4 demo: https://codepen.io/tkrotoff/pen/oWQeQR

  • Good news! I finally put together a full working demo in a repository that I can share. You can find it here: https://github.com/atomicobject/elegant-form-validation-react

  • David says:

    Hey Jordan, thanks for putting this together. It looks like a good fit for my project. I’m about to start implementing anyway, but I’m wondering whether you know of anyone who has tried to use this in a React-Native project and what issues (if any) they’ve run into.

    Regardless of whether I hear anything back I’ll try to write again to report what I find.

  • flockofmirrors says:

    I appreciate the example Jordan, this is really easy to understand.

    However I’m trying to create a rule where it checks against an api to see if the username exists. I understand that this is expecting a function, but the call is returning a promise.

    Anyone have suggestions on how to deal with a Promise in this situation? I included a gist:

    https://gist.github.com/anonymous/9ae8091be32baa744b380519c44aea23

  • sabareesh says:

    This is great I am still learning React, if I want to extent select similarly like text field how should i do it ? because it has child elements

  • leejh3224 says:

    This article is so inspiring. I always tried to seperate view logics with validation logics however failed to accomplish. How neat and clear approach! I’d like to try this soon.

  • Kishor says:

    Hi there

    I have used your validation method and its working perfectly. However I want to show the entire textview in red colour if its not valid. I really couldn’t work it out. I tried to set showError false in handleFiledChanged, but its effect on all textview. Can you please help me to solve this.

    Thanks

  • Dominik says:

    Hey!

    Very nice tutorial, I really like your simple and straight forward approach.

    Maybe I’ve something interesting for you and the readers of your blog:
    https://github.com/cat-react/form

    @cat-react/form is a simple yet powerful library which helps created validated forms in react.

    You don’t have to take care of executing validation rules yourself and also have the possibility to create dependencies between fields which trigger revalidation of each other.
    Give it a shot and I’d love to see some feedback! :)

  • Mickey Puri says:

    Thank you Jordan, just been thru it, it is very elegant, and I like the ability to write one’s own set of custom validators.

    I did suggest on your github that it may be worth showing errors after a field has been touched. So that can be an enhancement.

    Another minor tweak is the writing style of functions, eg. it may be easier to read the code if you didn’t have the block quotes when returning nested functions.

  • Binh Bui says:

    Really thorough and helpful tutorial. Securing the form, especially the password input has always been the most prioritized thing in a developer’s list.

    I wrote an article which could be interesting for you and your readers here. It’s a blog post featuring Polymer based password input web components. There is a feature comparison table in there, as well.

    https://vaadin.com/blog/top-five-web-components-for-password-inp-1

  • Comments are closed.