React: Validating Children
One of the (many) great things about React is that it offers validation of properties. This greatly aids in enforcing the API of your components. If you have a Quantity
component for example, that takes a numeric value
property which is required, you can use propTypes
to validate that your component received what it expected.
var Quantity = React.createClass({
propTypes: {
value: React.PropTypes.number.isRequired
},
render: function () {
return <input value={this.props.value}/>;
}
});
Now if the number
property is omitted, or the value isn't numeric, React will log a warning to the console.
'Warning: Failed propType: Required prop `value` was not specified in `Quantity`. Check the render method of `App`.'
All this is great when you need to validate the properties of a component, but how do you handle validating the children that are provided to your component? Specifically, how do you ensure that certain types of components are provided, or not provided to your component? Let's explore.
Enforcing a single child
First let's look at how you can ensure that a child is supplied to your component. Let's assume that your component expects to always have a child, and only one child. This can be accomplished by looking no further than the propTypes
that we already saw. A component's children are just properties after all.
var FormField = React.createClass({
propTypes: {
error: React.PropTypes.bool,
label: React.PropTypes.string.isRequired,
children: React.PropTypes.element.isRequired
},
render: function () {
var className = this.props.error ? 'error' : null;
return (
<div className={className}>
<div>{this.props.label}:</div>
{this.props.children}
</div>
);
}
});
With this code if a child isn't supplied, or if more than one child is supplied, a warning will occur.
// Warning: no children
var App = React.createClass({
render: function () {
return (
<div>
<FormField label="Email"/>
</div>
);
}
});
// Warning: too many children
var App = React.createClass({
render: function () {
return (
<div>
<FormField label="Email">
<input/>
<textarea></textarea>
</FormField>
</div>
);
}
});
// A-OK!
var App = React.createClass({
render: function () {
return (
<div>
<FormField label="Email">
<input/>
</FormField>
</div>
);
}
});
Validating the type of an element
Making sure that a single child is provided to your component is handy, but what if you need to validate the type of the child? In the example of our FormField
component, we want to limit children to one of input
, select
, or textarea
.
Again we can leverage React's propTypes
which offers the ability to use your own function
for validating a property.
var FormField = React.createClass({
propTypes: {
error: React.PropTypes.bool,
label: React.PropTypes.string.isRequired,
children: function (props, propName, componentName) {
var prop = props[propName];
var types = ['input', 'select', 'textarea'];
// Only accept a single child, of the appropriate type
if (React.Children.count(prop) !== 1 ||
types.indexOf(prop.type) === -1) {
return new Error(
'`' + componentName + '` ' +
'should have a single child of the following types: ' +
' `' + types.join('`, `') + '`.'
);
}
}
},
render: function () {
var className = this.props.error ? 'error' : null;
return (
<div className={className}>
<div>{this.props.label}:</div>
{this.props.children}
</div>
);
}
});
Now if something other than a single input
, select
, or textarea
is passed as the components children we will get a warning.
// Passes prop type validation
var App = React.createClass({
render: function () {
return (
<div>
<FormField label="Email">
<input type="text"/>
</FormField>
</div>
);
}
});
Validating the type of custom components
When validating a custom component there are a couple of choices. The first option uses prop.type.displayName
. This will be the value of the displayName
property on your component. If you omit that property on your component, and you are using JSX, it will be the name of the variable that you assign your React.createClass
to. To use this you would do something like:
prop.type.displayName === 'MyComponent'
The downside with this is that you are comparing a hard coded string. Should your component's displayName
change you would need to update your propType
validation.
The other option is to compare the component class directly:
prop.type === MyComponent
This is much nicer than using string comparison, and is ultimately what I'd recommend.
var FormField = React.createClass({
propTypes: {
error: React.PropTypes.bool,
label: React.PropTypes.string.isRequired,
children: function (props, propName, componentName) {
var prop = props[propName];
var types = ['input', 'select', 'textarea'];
// Only accept a single child, of the appropriate type
if (React.Children.count(prop) !== 1 || (
types.indexOf(prop.type) === -1 &&
prop.type !== RadioGroup
)
) {
return new Error(
'`' + componentName + '` ' +
'should have a single child of the following types: `' +
types.join('`, `') + '`, `RadioGroup`.'
);
}
}
},
render: function () {
var className = this.props.error ? 'error' : null;
return (
<div className={className}>
<div>{this.props.label}:</div>
{this.props.children}
</div>
);
}
});
var RadioGroup = React.createClass({
propTypes: {
children: function (props, propName, componentName) {
var prop = props[propName];
var types = ['radio', 'checkbox'];
var error;
React.Children.forEach(prop, function (el) {
if (error) return;
// Make sure child is a label
if (el.type !== 'label') {
error = new Error(
'`' + componentName + '` only accepts `labels`.'
);
}
// If child is a label, validate it's children
if (!error) {
var foundInput = 0;
React.Children.forEach(el.props.children, function (e) {
if (error) return;
// The label contains a single input, of appropriate type
if (e.type === 'input' &&
types.indexOf(e.props.type) > -1) {
foundInput++;
}
});
// If a single input wasn't found issue warning
if (foundInput !== 1) {
error = new Error(
'`' + componentName + '` ' +
'only accepts inputs of the following types: `' +
types.join('`, `') + '`.'
);
}
}
});
return error;
},
},
render: function () {
return (
<ul>
{this.props.children.map(function (item) {
return <li>{item}</li>;
})}
</ul>
);
}
});
With this code, our FormField
component will now accept input
, select
, textarea
, or our custom RadioGroup
component. Our RadioGroup
component will in turn validate that it's children are labels
each containing a single input
of type radio
, or checkbox
.
var App = React.createClass({
render: function () {
return (
<div>
<FormField label="Toppings">
<RadioGroup>
<label><input type="checkbox"/> Pepperoni</label>
<label><input type="checkbox"/> Olives</label>
<label><input type="checkbox"/> Mushrooms</label>
<label><input type="checkbox"/> Extra Cheese</label>
</RadioGroup>
</FormField>
</div>
);
}
});
Additional Reading
Open source hacker. Community organizer. Co-organizer @ReactRally. Software Sommelier.