Usando mixins vs componentes para reutilização de código no Facebook React

116

Estou começando a usar o Facebook React em um projeto de Backbone e até agora está indo muito bem.
No entanto, notei alguma duplicação se infiltrando em meu código React.

Por exemplo, tenho vários widgets semelhantes a formulários com estados como INITIAL, SENDINGe SENT. Quando um botão é pressionado, o formulário precisa ser validado, uma solicitação é feita e o estado é atualizado. O estado é mantido dentro do React this.state, é claro, junto com os valores dos campos.

Se fossem visualizações do Backbone, eu teria extraído uma classe base chamada, FormViewmas minha impressão era que o React não endossa nem apóia a criação de subclasses para compartilhar a lógica da visualização (corrija-me se estiver errado).

Eu vi duas abordagens para reutilização de código no React:

Estou correto que mixins e containers são preferidos à herança no React? Esta é uma decisão de design deliberada? Faria mais sentido usar um mixin ou um componente de contêiner para meu exemplo de “widget de formulário” do segundo parágrafo?

Aqui está uma essência com FeedbackWidgete JoinWidgetem seu estado atual . Eles têm uma estrutura semelhante, beginSendmétodo semelhante e ambos precisarão de algum suporte de validação (ainda não existe).

Dan Abramov
fonte
Como uma atualização para isso - a react está tendo dúvidas sobre o suporte de mixins no futuro, porque quando seus componentes, por exemplo, DidMount estão todos funcionando magicamente, o react está fazendo algumas coisas complicadas para que eles não substituam uns aos outros .. porque os mixins são muito simplista e inadequada
Dominic
Não tenho muita experiência com React, mas você poderia definir seu próprio mixin com funções que não se sobrepusessem ao namespace dos objetos React reais. em seguida, basta chamar as funções de objeto de "superclasse" / composição de suas funções de componentes React típicas. então não há sobreposição entre funções React e funções herdadas. isso ajuda a reduzir alguns clichês, mas limita a mágica acontecendo e torna mais fácil para o próprio React operar nos bastidores. isso é realmente tão difícil de conceber? Espero ter sido claro.
Alexander Mills
Os mixins nunca morrerão porque você sempre pode fazer mixins DIY. O React não terá suporte "nativo" para mixins, mas você ainda pode fazer mixins com JS nativo.
Alexander Mills

Respostas:

109

Atualização: esta resposta está desatualizada. Fique longe dos mixins, se puder. Eu te avisei!
Mixins estão mortos. Composição Longa ao Vivo

No início, tentei usar subcomponentes para isso e extrair FormWidgete InputWidget. No entanto, abandonei essa abordagem no meio do caminho porque queria um melhor controle sobre os inputs gerados e seu estado.

Dois artigos que mais me ajudaram:

Acontece que eu só precisava escrever dois mixins (diferentes): ValidationMixine FormMixin.
Veja como eu os separei.

ValidationMixin

O mixin de validação adiciona métodos de conveniência para executar suas funções de validador em algumas das propriedades de seu estado e armazenar propriedades "com erro" em um state.errors array para que você possa destacar os campos correspondentes.

Fonte ( essência )

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;

});

Uso

ValidationMixintem três métodos: validate, hasErrore resetError.
Ele espera que a classe defina o validatorsobjeto, semelhante a propTypes:

var JoinWidget = React.createClass({
  mixins: [React.addons.LinkedStateMixin, ValidationMixin, FormMixin],

  validators: {
    email: Misc.isValidEmail,
    name: function (name) {
      return name.length > 0;
    }
  },

  // ...

});

Quando o usuário pressiona o botão de envio, eu ligo validate. Uma chamada para validateirá executar cada validador e preencherthis.state.errors com uma matriz que contém as chaves das propriedades que falharam na validação.

No meu rendermétodo, eu uso hasErrorpara gerar classe CSS correta para campos. Quando o usuário coloca o foco dentro do campo, eu chamo resetErrorpara remover o destaque do erro até a próxima validatechamada.

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

O mixin de formulários controla o estado do formulário (editável, submetido, enviado). Você pode usá-lo para desabilitar entradas e botões enquanto a solicitação está sendo enviada e para atualizar sua visualização de acordo com o envio.

Fonte ( essência )

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;

});

Uso

Ele espera que o componente forneça um método: sendRequest que deve retornar uma promessa do Bluebird. (É trivial modificá-lo para funcionar com Q ou outra biblioteca de promessa.)

Ele fornece métodos de conveniência, como isFormEditable, isFormSubmittinge isFormSubmitted. Ele também fornece um método para lançar o pedido: submitForm. Você pode chamá-lo a partir do onClickmanipulador dos botões do formulário .

Dan Abramov
fonte
2
@jmcejuela Na verdade, mudei para uma abordagem mais componente mais tarde (ainda usando mixins intensamente), posso expandir isso em algum ponto ..
Dan Abramov
1
Há alguma notícia sobre "abordagem mais componente"?
NilColor
3
@NilColor Ainda não, não estou muito satisfeito com isso. :-) Atualmente tenho FormInputque falar com seu dono via formLink. formLinké como valueLink, e é devolvido a partir FormMixindo linkValidatedState(name, validator)método. FormInputobtém seu valor de formLink.valuee chama formLink.requestBlure formLink.requestFocus- eles causam a validação em FormMixin. Finalmente, para personalizar o componente real que está sendo usado para entrada, posso passá-lo para FormInput:<FormInput component={React.DOM.textarea} ... />
Dan Abramov
Boa resposta - algumas dicas: você não precisa chamar o donebluebird e o código funcionará como está no Q (ou promessas nativas) - é claro que o bluebird é melhor. Observe também que a sintaxe mudou no React desde a resposta.
Benjamin Gruenbaum
4

Estou construindo um SPA com React (em produção há 1 ano), e quase nunca uso mixins.

O único caso de uso que tenho atualmente para mixins é quando você deseja compartilhar o comportamento que usa os métodos de ciclo de vida do React ( componentDidMountetc). Este problema é resolvido pelos Componentes de Ordem Superior que Dan Abramov fala em seu link (ou usando a herança de classe ES6).

Mixins também são frequentemente usados ​​em frameworks, para tornar a API do framework disponível para todos os componentes, usando o recurso de contexto "oculto" do React. Isso não será mais necessário com a herança de classe ES6.


Na maioria das outras vezes, os mixins são usados, mas não são realmente necessários e podem ser substituídos por facilitadores por simples ajudantes.

Por exemplo:

var WithLink = React.createClass({
  mixins: [React.addons.LinkedStateMixin],
  getInitialState: function() {
    return {message: 'Hello!'};
  },
  render: function() {
    return <input type="text" valueLink={this.linkState('message')} />;
  }
});

Você pode refatorar facilmente o LinkedStateMixincódigo para que a sintaxe seja:

var WithLink = React.createClass({
  getInitialState: function() {
    return {message: 'Hello!'};
  },
  render: function() {
    return <input type="text" valueLink={LinkState(this,'message')} />;
  }
});

Existe alguma grande diferença?

Sebastien Lorber
fonte
Você está certo. Na verdade, os documentos do LinkedStateMixin explicam como fazer isso sem o mixin. Este mixin em particular é apenas um pouco de açúcar sintático.
nextgentech de