Je commence à utiliser Facebook React dans un projet Backbone et jusqu'à présent, tout se passe très bien.
Cependant, j’ai remarqué une duplication qui se glissait dans mon code React.
Par exemple, j'ai plusieurs widgets de type formulaire avec des états tels que INITIAL
, SENDING
et SENT
. Lorsque vous appuyez sur un bouton, le formulaire doit être validé, une demande est faite, puis l'état est mis à jour. L'état est conservé à l'intérieur React this.state
bien sûr, avec les valeurs de champ.
Si c'étaient des vues Backbone, j'aurais extrait une classe de base appelée FormView
mais j'avais l'impression que React n'endosse ni ne supporte les sous-classes partage la logique de vue (corrigez-moi si je me trompe).
J'ai vu deux approches de la réutilisation du code dans React:
Ai-je raison d'affirmer que les mixins et les conteneurs sont préférés à l'héritage dans React? Est-ce une décision de conception délibérée? Est-il plus logique d'utiliser un mixin ou un composant conteneur pour mon exemple de "widget de formulaire" du deuxième paragraphe?
Voici un Gist avec FeedbackWidget
et JoinWidget
dans leur état actuel . Ils ont une structure similaire, une méthode similaire beginSend
et auront tous deux besoin d’un support de validation (pas encore là).
Mise à jour: cette réponse est obsolète. Restez à l'écart des mixins si vous le pouvez. Je t'avais prévenu!
Les Mixins sont morts. Composition longue vie
Au début, j'ai essayé d'utiliser des sous-composants pour cela et d'extraire FormWidget
et InputWidget
. Cependant, j’ai abandonné cette approche à mi-parcours parce que je voulais un meilleur contrôle sur les input
s générés et leur état.
Deux articles qui m'ont le plus aidé:
Il s'est avéré que je n'avais besoin que d'écrire deux mixins (différents): ValidationMixin
et FormMixin
.
Voici comment je les ai séparés.
Le mixin de validation ajoute des méthodes pratiques pour exécuter vos fonctions de validation sur certaines propriétés de votre état et stocker les propriétés "error'd" dans un fichier state.errors
tableau afin que vous puissiez mettre en évidence les champs correspondants.
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
a trois méthodes: validate
, hasError
et resetError
.
La classe devrait définir l'objet validators
, semblable à propTypes
:
var JoinWidget = React.createClass({
mixins: [React.addons.LinkedStateMixin, ValidationMixin, FormMixin],
validators: {
email: Misc.isValidEmail,
name: function (name) {
return name.length > 0;
}
},
// ...
});
Lorsque l'utilisateur appuie sur le bouton de soumission, j'appelle validate
. Un appel à validate
exécutera chaque validateur et peuplera this.state.errors
avec un tableau contenant les clés des propriétés pour lesquelles la validation a échoué.
Dans ma méthode render
, j'utilise hasError
pour générer la classe CSS correcte pour les champs. Lorsque l'utilisateur place le focus dans le champ, j'appelle resetError
pour supprimer l'erreur en surbrillance jusqu'à l'appel suivant validate
.
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)} />
);
}
Formulaire de mélange gère l'état du formulaire (modifiable, soumis, soumis). Vous pouvez l'utiliser pour désactiver les entrées et les boutons lors de l'envoi de la demande et pour mettre à jour votre vue en conséquence lors de l'envoi.
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;
});
Il s'attend à ce que composant fournisse une méthode: sendRequest
, qui devrait renvoyer une promesse Bluebird. (Il est facile de le modifier pour qu'il fonctionne avec Q ou une autre bibliothèque de promesses.)
Il fournit des méthodes pratiques telles que isFormEditable
, isFormSubmitting
et isFormSubmitted
. Il fournit également une méthode pour lancer la requête: submitForm
. Vous pouvez l'appeler depuis le gestionnaire onClick
des boutons de formulaire.
Je construis un SPA avec React (en production depuis 1 an), et je n’utilise presque jamais de mixins.
La seule utilisation que je connaisse actuellement pour les mixins concerne le partage d'un comportement utilisant les méthodes du cycle de vie de React (componentDidMount
etc.). Ce problème est résolu par les composants d'ordre supérieur que Dan Abramov parle dans son lien (ou en utilisant l'héritage de classe ES6).
Les mixins sont également souvent utilisés dans les frameworks pour rendre l'API framework disponible à tous les composants, en utilisant le paramètre "hidden" fonctionnalité de contexte de React. Cela ne sera plus nécessaire non plus avec l'héritage de classe ES6.
La plupart du temps, les mixins sont utilisés, mais ne sont pas vraiment nécessaires et pourraient être remplacés plus facilement par de simples assistants.
Par exemple:
var WithLink = React.createClass({
mixins: [React.addons.LinkedStateMixin],
getInitialState: function() {
return {message: 'Hello!'};
},
render: function() {
return <input type="text" valueLink={this.linkState('message')} />;
}
});
Vous pouvez très facilement refactoriser le code LinkedStateMixin
afin que la syntaxe soit la suivante:
var WithLink = React.createClass({
getInitialState: function() {
return {message: 'Hello!'};
},
render: function() {
return <input type="text" valueLink={LinkState(this,'message')} />;
}
});
Y a-t-il une grande différence?