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.

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