Custom React Prop Validators


I just recently started learning to use the React library and one of the things I absolutely love is how you can provide rigid validation for props passed into a component. Although I see many examples where this is omitted, I believe this is important information to include - especially when on a project with multiple folks using the components. It is like specifying the api (so-to-speak) of your component right up front, so there is no guesswork in what the passed-in props should look like. While React has many built-in validators, there is also the option to write custom ones as needed.


Custom Validator API

There's a very simple API for how custom validators should work. We will just use a function that takes in three parameters:

  • props: This is the object hash of all properties passed into the component
  • propName: The name of the property currently being evaluated
  • componentName: The name of the component currently being worked with

The function will only return something if the validation fails. What will be returned is an Error - which looks like this: return new Error('some descriptive message here'); If validation passes, there's no need to return anything.


Custom Validator Example

In an app I was writing to help me learn React, I had a component that needed a certain property to be a moment object, as I use momentJS for working with dates. So, here's what our simple validator looks like:

//momentPropType.js
var moment = require('moment');

module.exports = function isPropMoment(props, propName, componentName) {  
    if (!moment.isMoment(props[propName])) {
        return new Error(
            [
                componentName,
                'requires that',
                propName,
                'be a Moment object.'
            ].join(' ')
        );
    }
};

//someComponent.js - using the validator
var momentPropType = require('momentPropType.js');

module.exports = React.createClass({  
  propTypes: {
    someDate: momentPropType
  }
});

And it's that simple. But, what if we want to extend that functionality to handle whether or not the value is required or not? Maybe we want to be able to do this instead: someDate: momentPropType.isRequired, just like the native React.PropTypes entries allow. So, I decided it would be a decently fun exercise to write my own implementation of this before looking at the source to see how React implements this. Although, after looking at the source I saw that the way React did it was actually better - I'll go over this at the end :)


'Wrapper' Function For Validators

A function in JavaScript is special - it can have additional properties just like any other object can. Also, references to them can be passed in as arguments to other functions. Using these powerful features, we can create a 'wrapper' function that extrapolates the isRequired functionality and then it can be used with any other custom validator we write! Here's how this looks:

//requirablePropType.js
//@param testFn {Function} used to test validity of the property
module.exports = function requirablePropType(testFn) {  
  function testProperty(props, propName, componentName) {
    var propVal = props[propName];
    var propIsEmpty = [undefined,null,''].indexOf(propVal) > -1;
    //extract very last argument to see if this is required
    var isRequired = arguments.length > 3 && arguments[arguments.length-1] === true;
    var error;

    if (isRequired === true && propIsEmpty) {
      error = new Error([
        componentName,
        'requires that',
        '"' + propName + '"',
        'be populated'
      ].join(' '));
    } else if (!(!isRequired && propIsEmpty)) {
      error = testFn(props, propName, componentName);
    }

    if (error !== undefined) {
      //only need to return if there's an error
      //undefined auto-returned if no return from a function
      return error;
    }
  }//end function testProperty

  //add 'isRequired' function to the testProperty function
  testProp.isRequired = function isRequiredProp() {
    Array.prototype.push.call(arguments, true);
    return testProp.apply(null, arguments);
  };

  return testProp; //this is a function that also has an 'isRequired' function!
};

Now, we can update our momentPropType validator to use this wrapper:

//momentPropType.js
var moment = require('moment');  
var requireablePropType = require('requirablePropType.js');

function isPropMoment(props, propName, componentName) {  
    if (!moment.isMoment(props[propName])) {
        return new Error(
            [
                componentName,
                'requires that',
                propName,
                'be a Moment object.'
            ].join(' ')
        );
    }
}

module.exports = requireablePropType(isPropMoment);


//someComponent.js - using the validator
var momentPropType = require('momentPropType.js');

module.exports = React.createClass({  
  propTypes: {
    someDate: momentPropType, //NOT required
    someRequiredDate: momentPropType.isRequired //REQUIRED
  }
});

Here's a quick fiddle I put together showing that this works:


Comparing Approaches

So, I had set out to accomplish what I wanted - making my own custom validators be able to handle it if the property were required or not and do it in such a way that each validator didn't have to repeat the isRequired logic. But, I wanted to see how the folks at React implemented this, and found that there's actually only one real difference between our implementations, but it's quite important.

Their function that actually tests the property (named checkType) has the isRequired parameter in first position instead of last position where I had put it. This turns out to be pretty slick because we can then take advantage of how function.bind works to more cleanly do this so that there is no parsing or manipulation of the arguments and not having to actually fill in the body of the isRequired property:

var chainedCheckType = checkType.bind(null, false);  
chainedCheckType.isRequired = checkType.bind(null, true);

return chainedCheckType;  

The result of this is that whether chainedCheckType or chainedCheckType.isRequired is the function called, the first parameter (isRequired) is already bound to the appropriate value, so the remaining parameters are what the React ecosystem expects - props, propName, and componentName. Pretty cool!

-Bradley

Loading Google+ Comments ...