Использование mixins vs для повторного использования кода в Facebook React

Я начинаю использовать Facebook React в проекте Backbone, и пока все идет хорошо. Тем не менее, я заметил, что некоторые дублирования ползут в моем коде React.

Например, У меня есть несколько виджетов вида с такими состояниями, как INITIAL, SENDING и SENT. При нажатии кнопки форма должна быть проверена, делается запрос, а затем состояние обновляется. Состояние сохраняется внутри React this.state, конечно, вместе с значениями поля.

Если бы это были представления Backbone, я бы извлек базовый класс под названием FormView, но мое впечатление было то, что React не поддерживает и не поддерживает подклассы, чтобы обмениваться логикой представления (исправьте меня, если я ошибаюсь).

Я видел два подхода к повторному использованию кода в React:

Правильно ли, что миксины и контейнеры предпочтительнее наследования в React? Это преднамеренное дизайнерское решение? Было бы разумнее использовать mixin или компонент контейнера для моего примера виджета формы из второго абзаца?

Вот суть с FeedbackWidget и JoinWidget в их текущем состоянии. Они имеют аналогичную структуру, аналогичный метод beginSend, и оба должны иметь некоторую поддержку проверки (еще нет).

Ответ 1

Обновление: этот ответ устарел. Держитесь подальше от миксинов, если сможете. Я предупреждал тебя! Микшины мертвы. Длительный живой состав

Сначала я попытался использовать подкомпоненты для этого и извлечь FormWidget и InputWidget. Тем не менее, я отказался от этого подхода на полпути, потому что мне нужен лучший контроль над сгенерированными input и их состоянием.

Две статьи, которые помогли мне больше всего:

Оказалось, что мне нужно было написать только два (разных) mixins: ValidationMixin и FormMixin.
Вот как я их отделил.

ValidationMixin

Validation mixin добавляет удобные методы для запуска ваших функций проверки правильности некоторых свойств вашего состояния и хранения свойств "error'd" в массиве state.errors, чтобы вы могли выделить соответствующие поля.

Источник (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;

});

Использование

ValidationMixin имеет три метода: validate, hasError и resetError.
Он ожидает, что класс определит объект validators, аналогичный propTypes:

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

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

  // ...

});

Когда пользователь нажимает кнопку отправки, я вызываю validate. Вызов validate будет запускать каждый валидатор и заполнить this.state.errors массивом, который содержит ключи свойств, которые не прошли проверку.

В моем методе render я использую hasError для создания правильного CSS-класса для полей. Когда пользователь помещает фокус в поле, я вызываю resetError, чтобы удалить выделение ошибки до следующего вызова 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

Форма mixin обрабатывает форму состояния (редактируется, отправляется, отправляется). Вы можете использовать его для отключения входов и кнопок во время отправки запроса, а также для обновления вашего представления при его отправке.

Источник (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;

});

Использование

Он ожидает, что компонент предоставит один метод: sendRequest, который должен вернуть обещание Bluebird. (Это тривиально, чтобы изменить его для работы с Q или другой библиотекой обещаний.)

Он предоставляет удобные методы, такие как isFormEditable, isFormSubmitting и isFormSubmitted. Он также предоставляет метод для запуска запроса: submitForm. Вы можете вызвать его из обработчика формы onClick.

Ответ 2

Я создаю SPA с реактивом (в производстве с 1 года), и я почти никогда не использую mixins.

Единственное, что я сейчас использую для mixins, - это когда вы хотите делиться поведением, которое использует методы жизненного цикла React (componentDidMount и т.д.). Эта проблема решается компонентами более высокого порядка, которые Дэн Абрамов говорит в своей ссылке (или с помощью наследования класса ES6).

Mixins также часто используются в рамках, чтобы сделать API-интерфейс Framework доступным для всех компонентов, используя контекстную функцию , Это больше не понадобится либо с наследованием класса ES6.


В большинстве других случаев используются микшины, но на самом деле они не нужны, и их можно заменить простым помощником.

Например:

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

Вы можете легко отредактировать код LinkedStateMixin так, чтобы синтаксис был следующим:

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

Есть ли какая-то большая разница?