Uso de mixins vs componentes para la reutilización de código en Facebook React

116

Estoy empezando a usar Facebook React en un proyecto de Backbone y hasta ahora me va muy bien.
Sin embargo, noté algunas duplicaciones en mi código de React.

Por ejemplo, tengo varios widgets en forma de formulario con estados como INITIAL, SENDINGy SENT. Cuando se presiona un botón, el formulario debe validarse, se realiza una solicitud y luego se actualiza el estado. El estado se mantiene dentro de React, this.statepor supuesto, junto con los valores de campo.

Si estas fueran vistas de Backbone, habría extraído una clase base llamada, FormViewpero mi impresión fue que React no respalda ni admite subclases para compartir la lógica de la vista (corríjame si me equivoco).

He visto dos enfoques para la reutilización de código en React:

¿Tengo razón en que se prefieren los mixins y los contenedores a la herencia en React? ¿Es esta una decisión de diseño deliberada? ¿Tendría más sentido utilizar un componente mixin o contenedor para mi ejemplo de "widget de formulario" del segundo párrafo?

Aquí hay una esencia con FeedbackWidgety JoinWidgeten su estado actual . Tienen una estructura similar, un beginSendmétodo similar y ambos necesitarán algún soporte de validación (todavía no existe).

Dan Abramov
fuente
Como una actualización de esto, react está teniendo dudas sobre el soporte de mixins en el futuro, porque cuando tus componentes, por ejemplo, DidMount funcionan mágicamente, reaccionan haciendo cosas complicadas para que no se sobrescriban entre sí ... porque los mixins son muy simplista y no apto para su propósito
Domingo
No tengo mucha experiencia con React, pero podrías definir tu propia mezcla con funciones que no se superpongan con el espacio de nombres de los objetos React reales. luego simplemente llame a las funciones de objeto de composición / "superclase" desde sus funciones típicas de componentes de React. entonces no hay superposición entre las funciones de React y las funciones heredadas. esto ayuda a reducir algo de repetición, pero limita la magia que ocurre y hace que sea más fácil para React operar detrás de escena. ¿Es esto realmente tan difícil de concebir? Espero haberme dejado claro.
Alexander Mills
Los mixins nunca morirán porque siempre puedes hacer mixins de bricolaje. React simplemente no tendrá soporte "nativo" para mixins, pero aún puede hacer mixins usted mismo con JS nativo.
Alexander Mills

Respuestas:

109

Actualización: esta respuesta está desactualizada. Manténgase alejado de los mixins si puede. ¡Te lo adverti!
Los mixins están muertos. Composición Larga Vida

Al principio, traté de usar subcomponentes para esto y extraer FormWidgety InputWidget. Sin embargo, abandoné este enfoque a la mitad porque quería un mejor control sobre los mensajes generados inputy su estado.

Dos artículos que más me ayudaron:

Resultó que solo necesitaba escribir dos mixins (diferentes): ValidationMixiny FormMixin.
Así es como los separé.

ValidaciónMixin

Validation mixin agrega métodos convenientes para ejecutar sus funciones de validación en algunas de las propiedades de su estado y almacenar propiedades "con error" en una state.errorsmatriz para que pueda resaltar los campos correspondientes.

Fuente ( esencia )

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

ValidationMixintiene tres métodos: validate, hasErrory resetError.
Espera que la clase defina el validatorsobjeto, similar a propTypes:

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

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

  // ...

});

Cuando el usuario presiona el botón de envío, llamo validate. Una llamada a validateejecutará cada validador y se completará this.state.errorscon una matriz que contiene claves de las propiedades que fallaron en la validación.

En mi rendermétodo, utilizo hasErrorpara generar la clase CSS correcta para los campos. Cuando el usuario pone el foco dentro del campo, llamo resetErrorpara eliminar el resaltado de error hasta la próxima validatellamada.

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 maneja el estado del formulario (editable, envío, enviado). Puede usarlo para deshabilitar entradas y botones mientras se envía la solicitud, y para actualizar su vista en consecuencia cuando se envía.

Fuente ( esencia )

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

Espera que el componente proporcione un método:, sendRequestque debería devolver una promesa Bluebird. (Es trivial modificarlo para que funcione con Q u otra biblioteca de promesas).

Proporciona métodos de conveniencia como isFormEditable, isFormSubmittingy isFormSubmitted. También proporciona un método para dar inicio a la solicitud: submitForm. Puede llamarlo desde el onClickcontrolador de botones de formulario .

Dan Abramov
fuente
2
@jmcejuela De hecho, pasé a un enfoque más basado en componentes más tarde (todavía usando mucho mixins), podría ampliar esto en algún momento ..
Dan Abramov
1
¿Hay alguna noticia sobre un "enfoque más basado en componentes"?
NilColor
3
@NilColor Todavía no, no estoy del todo satisfecho con él. :-) Actualmente tengo FormInputque habla con su dueño vía formLink. formLinkes como valueLink, y se devuelve desde FormMixin's linkValidatedState(name, validator)método. FormInputobtiene su valor de formLink.valuey llama formLink.requestBlury formLink.requestFocus—causan validación en FormMixin. Finalmente, para personalizar el componente real que se utiliza para la entrada, puedo pasarlo a FormInput:<FormInput component={React.DOM.textarea} ... />
Dan Abramov
Buena respuesta, algunos consejos: no tiene que llamar donea bluebird y el código funcionará como está en Q (o promesas nativas); por supuesto, bluebird es mejor. También tenga en cuenta que la sintaxis cambió en React desde la respuesta.
Benjamin Gruenbaum
4

Estoy construyendo un SPA con React (en producción desde 1 año) y casi nunca uso mixins.

El único caso de uso que tengo actualmente para mixins es cuando desea compartir un comportamiento que usa los métodos del ciclo de vida de React ( componentDidMountetc.). Este problema se resuelve con los componentes de orden superior que Dan Abramov habla en su enlace (o mediante el uso de la herencia de clases ES6).

Los mixins también se utilizan a menudo en marcos, para hacer que la API del marco esté disponible para todos los componentes, mediante el uso de la función de contexto "oculta" de React. Esto tampoco será necesario con la herencia de clases ES6.


La mayoría de las otras veces, se usan mixins, pero en realidad no son necesarios y podrían reemplazarse más fácilmente con ayudantes simples.

Por ejemplo:

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

Puede refactorizar muy fácilmente el LinkedStateMixincódigo para que la sintaxis sea:

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

¿Existe alguna gran diferencia?

Sebastien Lorber
fuente
Estás en lo correcto. De hecho, los documentos de LinkedStateMixin realmente explican cómo hacerlo sin el mixin. Este mixin en particular es en realidad solo un poco de azúcar sintáctico.
nextgentech