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

116

Je commence à utiliser Facebook React dans un projet Backbone et jusqu'à présent ça se passe très bien.
Cependant, j'ai remarqué une duplication s'insinuant dans mon code React.

Par exemple, j'ai plusieurs widgets de type formulaire avec des états comme INITIAL, SENDINGet SENT. Lorsqu'un bouton est enfoncé, le formulaire doit être validé, une demande est effectuée, puis l'état est mis à jour. L'état est this.statebien sûr conservé dans React , avec les valeurs de champ.

Si c'étaient des vues Backbone, j'aurais extrait une classe de base appelée FormViewmais mon impression était que React n'approuve ni ne prend en charge le sous-classement pour partager 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 de dire que les mixins et les conteneurs sont préférés à l'héritage dans React? Est-ce une décision délibérée de conception? Serait-il plus logique d'utiliser un mixin ou un composant conteneur pour mon exemple de «widget de formulaire» du deuxième paragraphe?

Voici un aperçu de FeedbackWidgetet JoinWidgetdans leur état actuel . Ils ont une structure similaire, une beginSendméthode similaire et devront tous deux avoir un support de validation (pas encore là).

Dan Abramov
la source
En guise de mise à jour, réagissez avoir des doutes quant à la prise en charge des mixins dans le futur, car lorsque vos composants, par exemple, componentDidMount fonctionnent tous comme par magie, réagit fait des choses compliquées afin qu'ils ne s'écrasent pas ... parce que les mixins sont très simpliste et pas adapté à l'usage
Dominic
Je n'ai pas beaucoup d'expérience avec React, mais vous pouvez définir votre propre mixin avec des fonctions qui ne chevauchent pas l'espace de noms des objets React réels. puis appelez simplement les fonctions d'objet "superclasse" / composition à partir de vos fonctions de composants React typiques. alors il n'y a pas de chevauchement entre les fonctions React et les fonctions héritées. cela aide à réduire certains passe-partout, mais limite la magie et facilite le fonctionnement de React lui-même dans les coulisses. est-ce vraiment difficile à concevoir? J'espère que je me suis fait clairement comprendre.
Alexander Mills
Les mixins ne mourront jamais car vous pouvez toujours faire des mixins de bricolage. React n'aura tout simplement pas de support "natif" pour les mixins, mais vous pouvez toujours faire des mixins vous-même avec JS natif.
Alexander Mills

Réponses:

109

Mise à jour: cette réponse est obsolète. Éloignez-vous 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 FormWidgetet InputWidget. Cependant, j'ai abandonné cette approche à mi-chemin car je voulais un meilleur contrôle sur les générés inputet 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): ValidationMixinet FormMixin.
Voici comment je les ai séparés.

ValidationMixin

Validation mixin ajoute des méthodes pratiques pour exécuter vos fonctions de validation sur certaines des propriétés de votre état et stocker les propriétés «d'erreur» dans un state.errorstableau afin que vous puissiez mettre en évidence les champs correspondants.

Source ( essentiel )

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

ValidationMixina trois méthodes: validate, hasErroret resetError.
Il s'attend à ce que la classe définisse l' validatorsobjet, similaire à 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 à validateexécutera chaque validateur et remplira this.state.errorsavec un tableau contenant les clés des propriétés dont la validation a échoué.

Dans ma renderméthode, j'utilise hasErrorpour générer une classe CSS correcte pour les champs. Lorsque l'utilisateur met le focus dans le champ, j'appelle resetErrorpour supprimer la surbrillance d'erreur jusqu'au prochain validateappel.

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

Form mixin gère l'état du formulaire (modifiable, soumis, soumis). Vous pouvez l'utiliser pour désactiver les entrées et les boutons pendant l'envoi de la demande, et pour mettre à jour votre vue en conséquence lorsqu'elle est envoyée.

Source ( essentiel )

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 le composant fournisse une méthode:, sendRequestqui devrait renvoyer une promesse Bluebird. (Il est simple 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, isFormSubmittinget isFormSubmitted. Il fournit également une méthode pour lancer la demande: submitForm. Vous pouvez l'appeler à partir du onClickgestionnaire des boutons de formulaire .

Dan Abramov
la source
2
@jmcejuela En fait, je suis passé à une approche plus composants plus tard (toujours en utilisant beaucoup de mixins), je pourrais développer cela à un moment donné ..
Dan Abramov
1
Y a-t-il des nouvelles sur "une approche plus composante-ish"?
NilColor
3
@NilColor Pas encore, je ne suis pas tout à fait satisfait. :-) Actuellement, j'ai FormInputqui parle à son propriétaire via formLink. formLinkest similaire à valueLink, et est renvoyé par FormMixinla linkValidatedState(name, validator)méthode de.FormInputtire sa valeur de formLink.valueet appelle formLink.requestBluret formLink.requestFocus- ils provoquent la validation dans FormMixin. Enfin, pour personnaliser le composant utilisé pour l'entrée, je peux le transmettre à FormInput:<FormInput component={React.DOM.textarea} ... />
Dan Abramov
Bonne réponse - quelques conseils: vous n'avez pas besoin d'appeler donebluebird et le code fonctionnera tel quel dans Q (ou promesses natives) - bien sûr, bluebird est meilleur. Notez également que la syntaxe a changé dans React depuis la réponse.
Benjamin Gruenbaum
4

Je construis un SPA avec React (en production depuis 1 an), et je n'utilise presque jamais de mixins.

Le seul cas d'utilisation que j'ai actuellement pour les mixins est lorsque vous souhaitez partager un comportement qui utilise les méthodes de cycle de vie de React ( componentDidMountetc.). 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 du framework disponible à tous les composants, en utilisant la fonctionnalité de contexte "caché" 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 facilement remplacés par de simples helpers.

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 LinkedStateMixincode afin que la syntaxe soit:

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?

Sébastien Lorber
la source
Vous avez raison. En fait, la documentation LinkedStateMixin explique en fait comment le faire sans le mixin. Ce mixin particulier n'est en réalité qu'un petit sucre syntaxique.
nextgentech