Mixin' it up with ReactJS
I'm primarily an Angular developer, but recently I've started digging into React. One of the React concepts I've started enjoying is the notion of mixins.
Mixins are not a new concept and if you've used CSS pre-processors like LESS, SASS, or Stylus then you're likely already familiar with them. Rather than taking an object and extending it to include all the functionality of a different object, you can break out only the truly reusable parts into a mixin and composite that functionality into the appropriate objects. While this may seem like inheritance, it is more along the lines of implementing specific interfaces - only that the interfaces already have concrete implementations defined.
Mixins are like interfaces with concrete implementations already defined
Let's take a real-world example of having an input form. What if we could have this form work such that any field (or field container - we'll look at this in a future post) can notify the form when it became a part of the form, when its validation changed, and when it exited the form (i.e. in the case of dynamic addition/removal of fields based on input of other fields). Just from this alone, if I were doing a C# project I could see two potential interfaces:
- IRegisteredFormField - Understands that it should handle (un)registering itself with an owning container
- INotifyValidityChanged - Understands that it should notify an owning container any time its validity changes
The Form
The first thing we need to do is to create our form component. This form component will be given one specific property in order to function at a bare-bones level and to keep this post simple:
- onSubmit [function] - Event handler for when the form submits
One important aspect of our custom form implementation is that is will simply be a wrapper for whatever contents listed inside of it. We'll be using this.props.children
within the form's definition in order to access those children. This is important because the form itself defines how to handle (un)registration of inputs - so it has to essentially tell the child components how to do this. We'll use React.Children.map
and React.cloneElement
helpers in order to accomplish this. So, let's get coding:
//tell React to create a class, and then pass it a definition object
window.formComponent = React.createClass({
//tell it the props and their types to expect, only one is required
propTypes: { onSubmit: React.PropTypes.func.isRequired },
//container to hold all registered inputs
inputs: {},
//wire up registration events, note that a name is expected
registerInput: function(input) {
if ('name' in input) {
this.inputs[input.name] = input;
this.updateValidity();
}
},
deRegisterInput: function(input) {
//can pass in entire object, or just the name
var name = (typeof input === 'string') ? input : 'name' in input ? input.name : undefined;
if (name !== undefined && this.inputs[name]) {
delete this.inputs[name];
//want to update validity as the exiting field may have been the only invalid field
this.updateValidity();
}
},
validityUpdateTimer: undefined,
//wire up some validity events - the form controls whether the submit btn is enabled
//so it needs to be kept up to date with validity changes
//however, utilize timers so we aren't polling all registered fields for validity too often
updateValidity: function() {
if (this.validityUpdateTimer !== undefined) {
window.clearTimeout(this.validityUpdateTimer);
}
this.validityUpdateTime = window.setTimeout(function updateValidity() {
this.validityUpdateTimer = undefined;
//setState is the React way of notifying that state may have changed
this.setState({isValid: this.isValid()});
}.bind(this), 50);
},
isValid: function() {
//if every registerd input that has a validate() method returns true, we're valid
return Object.keys(this.inputs).every(function getValidity(key) {
var input = this.inputs[key];
if (typeof input.isValid === 'function') { return input.isValid(); }
return true;
}, this);
},
//events to get the full value of the form, and to submit it if it's valid
getValue: function() {
//just like with the isValid check, we're expecting registered components
//to have a specific interface
return Object.key(this.inputs).reduce(function getAllValues(valueStore, key) {
var input = this.inputs[key];
if (typeof input.getValue === 'function') { valueStore[key] = input.getValue(); }
return valueStore;
}.bind(this), {});
},
submitForm: function() {
if (this.isValid()) {
this.props.onSubmit(this.getValue());
}
}
});
Now, if you're already familiar with React, you'll know that I don't have a very important method listed in my component definition. This is intentional, as I want to point out how critical this method is in nature because it is a React lifecycle event:
- render [function] - This is a function that essentially tells React how to render the component.
Let's add render
to our form component right below the submitForm
function:
render: function () {
//here's where the action is - if we don't get this right, then all is lost ;-)
//first, assert the class to apply to the form - either valid or invalid
var formClass = (this.state.isValid === true ? '' : 'in') + 'valid';
//next, let's use React.Children.map and React.cloneElement helpers to "teach"
//the children how to interact with the form (children accessed by this.props.children)
var clonedChildren = React.Children.map(this.props.children, function clone(c) {
//React.cloneElement is a lot like Angular's transclude, if you're familiar with that.
//Essentially, we can populate some additional properties onto the children from here
var addedProps = {
registerToForm: this.registerInput,
deRegisterFromForm: this.deRegisterInput,
onValidityChanged: this.updateValidity
};
return React.cloneElement(c, addedProps);
}.bind(this));
//now, we just return the markup (using JSX here)
return (
<form name={this.props.name} className={formClass} onSubmit={this.props.onSubmit}>
{clonedChildren}
<div className="form-field buttons">
<button type="submit" onClick={this.submitForm} disabled={!this.state.isValid}>Submit</button>
</div>
</form>
);
}
Where you see {clonedChildren}
is where items will get dynamically injected - again, much like Angular's "transclude" functionality. However, the form component has control over the buttons and we could've totally used added props to control what the label for the submit button is, if there's a cancel button (and its label), etc...
Up to now, all we've really done is created a wrapper for a form
element so that we can give it some extra functionality. Now, we need to create our mixins, and then create a wrapper for an input field so it can use those mixins.
On to the mixins
Fortunately, React just uses POJO's (Plain Old Javascript Objects) for mixins. I prefer this to just using extend
to share functionality. The important thing to keep in mind is that any functions the mixin defines will automatically have its context (the this
keyword) scoped to the instance of the component it's applied to. If that doesn't make sense, hopefully the code will clear it up a bit :)
Registered Form Input Mixin
window.registeredFormInput = {
register: function() {
if (typeof this.props.registerToForm === 'function') {
//register, but only provide access to necessary items - don't just pass back "this"
//bind functions back to the current scope, as we're creating a new object here
this.props.registerToForm({
name: this.props.name,
isValid: function() {
return this.state.isValid;
}.bind(this),
getValue: function() {
return this.state.value;
}.bind(this)
});
}
},
deRegister: function() {
if (typeof this.props.deRegisterFromForm === 'function') {
this.props.deRegisterFromForm(this.props.name);
}
}
};
Validation Notifier Mixin
window.validityChangeNotifier = {
notifyValidityChanged: function() {
if (typeof this.props.onValidityChanged === 'function') {
this.props.onValidityChanged();
}
}
};
Using the mixins
Now that we've created the form container and the mixins, we have to create a wrapper for any input we'd like in order to take advantage of the functionality we've created. This is a double-edged sword - we could have custom input types that are built for super specific purposes (equivalent of Angular directives), but we have to write boiler-plate code to generate the definition for each. We'll just do a simple text input where the only constraint on it is that it can be a required field :)
window.simpleTextInput = React.createClass({
//use those mixins!
//So much better than just using an "extend" approach - no question here what
//additional functionality is being applied to this component!
mixins: [
window.registeredFormInput,
window.validityChangeNotifier
],
propTypes: {
name: React.PropTypes.string.isRequired, //has to have a name
isRequired: React.PropTypes.bool,
value: React.PropTypes.string
},
getDefaultProps: function() {
//set our own defaults for any props not populated
return {
isRequired: false,
value: ''
};
},
//this is another lifecycle method in the React ecosystem
getInitialState: function() {
return {
isValid: this.props.isRequired ? this.props.value.length > 0 : true,
value: this.props.value
};
},
//event handler for when the input value changes
onChange: function(event) {
var value = event.target.value;
var isValid = this.props.isRequired ? value.length > 0 : true;
//want to know if our validity changed, so check it before we update state
var validityDidChange = isValid !== this.state.isValid;
//setState can take a callback that fires *after* the state has been updated
//since our mixin functions reference the state, we want the latest values
this.setState({isValid: isValid, value: value}, function stateChanged() {
if (validityDidChange) {
this.notifyValidityChanged();
}
});
},
//next, wire up these React lifecycle events:
//componentWillMount, render, and componentWillUnmount
componentWillMount: function() {
//use the mixin 'register' method
this.register();
},
render: function() {
var className = (this.state.isValid ? '' : 'in') + 'valid';
return <input type="text" name="this.props.name" className={className} onChange={this.onChange} value={this.state.value} />;
},
componentWillUnmount: function() {
//this event is just like componentWillMount, but in the inverse situation:
//the component instance is about to be destroyed and its DOM representation removed
//so, use the mixin to remove registration from the form!
this.deRegsiter();
}
});
Putting it all together
The only thing left for us to do is to put it all together into a view that contains all of these ingredients mixin'd together - see what I did there? :p
var superSimpleForm = React.createClass({
onSubmit: function() {
//access child classes by adding a ref to them, then it's added to this.refs
console.log("Form values: ", this.refs.simpleForm.getValue());
},
render: function() {
return (
<window.formComponent ref="simpleForm" onSubmit={this.onSubmit}>
<label htmlFor="name">Name</label>
<window.simpleTextInput name="name" isRequired={true} />
<br/>
<label htmlFor="description">Description</label>
<window.simpleTextInput name="description" />
</window.formComponent>
);
}
});
Only one of the fields is required, so as long as that one is filled in the form should allow the submit button to be clicked!
Gotchas!
Quick note - this approach doesn't support nesting of elements inside the window.formComponent
. For example if render()
returned this it wouldn't work:
<window.formComponent ref="simpleForm" onSubmit={this.onSubmit}>
<div className="form-field">
<label htmlFor="name">Name</label>
<window.simpleTextInput name="name" isRequired={true} />
</div>
<div className="form-field">
<label htmlFor="description">Description</label>
<window.simpleTextInput name="description" />
</div>
</window.formComponent>
This is by design. React is all about making components, and while it is possible to recursively loop through all children and start setting properties, I really view that as an anti-pattern. The better approach would to be to create a component wrapper that can return the <div className="form-field">
and auto-populate the <label>
based on a property, and then uses {this.props.children}
to render all child inputs. That render()
method may look something like this:
render: function() {
//generate the label element and
//further propagate registration and validity-change props
//passed in from the form component onto children
return (
<div className="form-field">
<label htmlFor={this.props.name}>{this.props.label}</label>
{
React.Children.map(this.props.children, function(c) {
return React.cloneElement(c, {
registerToForm: this.registerInput,
deRegisterFromForm: this.deRegisterInput,
onValidityChanged: this.updateValidity
});
}, this)
}
</div>
);
}
This wrapper would have to propagate the props passed in from the form - but I think this is OK since the wrapper would specifically be for the purpose of making specific form-field layouts. In a future post, we'll look at using such a wrapper to populate error messages ;-)
Check out the , and share your thoughts in the comments!
-Bradley