web-dev-qa-db-fra.com

Utilisation de mixins vs composants pour la réutilisation de code dans Facebook React

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à).

115
Dan Abramov

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 inputs 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.

ValidationMixin

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.

Source ( Gist )

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;

});

Usage

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)} />
  );
}

FormMixin

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.

Source ( Gist )

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;

});

Usage

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.

108
Dan Abramov

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?

4
Sebastien Lorber