Update: this answer is outdated. Stay away from the mixins if you can. I warned you!
Mixins Are Dead. Long Live Composition
At first, I tried to use subcomponents for this and extract FormWidget
and InputWidget
. However, I abandoned this approach halfway because I wanted a better control over generated input
s and their state.
Two articles that helped me most:
It turned out to that I only needed to write two (different) mixins: ValidationMixin
and FormMixin
.
Here's how I separated them.
Validation mixin adds convenience methods to run your validator functions on some of your state's properties and store “error'd” properties in a state.errors
array so you can highlight corresponding fields.
define(function () {
'use strict';
var _ = require('underscore');
var ValidationMixin = {
getInitialState: function () {
return {
errors: []
};
},
componentWillMount: function () {
this.assertValidatorsDefined();
},
assertValidatorsDefined: function () {
if (!this.validators) {
throw new Error('ValidatorMixin requires this.validators to be defined on the component.');
}
_.each(_.keys(this.validators), function (key) {
var validator = this.validators[key];
if (!_.has(this.state, key)) {
throw new Error('Key "' + key + '" is defined in this.validators but not present in initial state.');
}
if (!_.isFunction(validator)) {
throw new Error('Validator for key "' + key + '" is not a function.');
}
}, this);
},
hasError: function (key) {
return _.contains(this.state.errors, key);
},
resetError: function (key) {
this.setState({
'errors': _.without(this.state.errors, key)
});
},
validate: function () {
var errors = _.filter(_.keys(this.validators), function (key) {
var validator = this.validators[key],
value = this.state[key];
return !validator(value);
}, this);
this.setState({
'errors': errors
});
return _.isEmpty(errors);
}
};
return ValidationMixin;
});
ValidationMixin
has three methods: validate
, hasError
and resetError
.
It expects class to define validators
object, similar to propTypes
:
var JoinWidget = React.createClass({
mixins: [React.addons.LinkedStateMixin, ValidationMixin, FormMixin],
validators: {
email: Misc.isValidEmail,
name: function (name) {
return name.length > 0;
}
},
// ...
});
When user presses the submission button, I call validate
. A call to validate
will run each validator and populate this.state.errors
with an array that contains keys of the properties that failed validation.
In my render
method, I use hasError
to generate correct CSS class for fields. When user puts focus inside the field, I call resetError
to remove error highlight till next validate
call.
renderInput: function (key, options) {
var classSet = {
'Form-control': true,
'Form-control--error': this.hasError(key)
};
return (
<input key={key}
type={options.type}
placeholder={options.placeholder}
className={React.addons.classSet(classSet)}
valueLink={this.linkState(key)}
onFocus={_.partial(this.resetError, key)} />
);
}
Form mixin handles form state (editable, submitting, submitted). You can use it to disable inputs and buttons while request is being sent, and to update your view correspondingly when it is sent.
define(function () {
'use strict';
var _ = require('underscore');
var EDITABLE_STATE = 'editable',
SUBMITTING_STATE = 'submitting',
SUBMITTED_STATE = 'submitted';
var FormMixin = {
getInitialState: function () {
return {
formState: EDITABLE_STATE
};
},
componentDidMount: function () {
if (!_.isFunction(this.sendRequest)) {
throw new Error('To use FormMixin, you must implement sendRequest.');
}
},
getFormState: function () {
return this.state.formState;
},
setFormState: function (formState) {
this.setState({
formState: formState
});
},
getFormError: function () {
return this.state.formError;
},
setFormError: function (formError) {
this.setState({
formError: formError
});
},
isFormEditable: function () {
return this.getFormState() === EDITABLE_STATE;
},
isFormSubmitting: function () {
return this.getFormState() === SUBMITTING_STATE;
},
isFormSubmitted: function () {
return this.getFormState() === SUBMITTED_STATE;
},
submitForm: function () {
if (!this.isFormEditable()) {
throw new Error('Form can only be submitted when in editable state.');
}
this.setFormState(SUBMITTING_STATE);
this.setFormError(undefined);
this.sendRequest()
.bind(this)
.then(function () {
this.setFormState(SUBMITTED_STATE);
})
.catch(function (err) {
this.setFormState(EDITABLE_STATE);
this.setFormError(err);
})
.done();
}
};
return FormMixin;
});
It expects component to provide one method: sendRequest
, which should return a Bluebird promise. (It's trivial to modify it to work with Q or other promise library.)
It provides convenience methods such as isFormEditable
, isFormSubmitting
and isFormSubmitted
. It also provides a method to kick off the request: submitForm
. You can call it from form buttons' onClick
handler.
I'm building an SPA with React (in production since 1 year), and I almost never use mixins.
The only usecase I currently have for mixins is when you want to share behavior that uses React's lifecycle methods (componentDidMount
etc). This problem is solved by the Higher-Order Components that Dan Abramov talk in his link (or by using ES6 class inheritance).
Mixins are also often used in frameworks, to make framework API available to all the components, by using the "hidden" context feature of React. This won't be needed anymore either with ES6 class inheritance.
Most of the other times, mixins are used, but are not really needed and could be easiler replaced with simple helpers.
For example:
var WithLink = React.createClass({
mixins: [React.addons.LinkedStateMixin],
getInitialState: function() {
return {message: 'Hello!'};
},
render: function() {
return <input type="text" valueLink={this.linkState('message')} />;
}
});
You can very easily refactor LinkedStateMixin
code so that the syntax would be:
var WithLink = React.createClass({
getInitialState: function() {
return {message: 'Hello!'};
},
render: function() {
return <input type="text" valueLink={LinkState(this,'message')} />;
}
});
Is there any big difference?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With