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

116

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

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

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

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

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

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

Дан Абрамов
источник
В качестве обновления к этому - у React есть сомнения о поддержке миксинов в долгом будущем, потому что, когда ваши, например, componentDidMount все просто волшебно работают, реакция делает некоторые сложные вещи, поэтому они не перезаписывают друг друга .. потому что миксины очень упрощенный и не соответствующий цели
Доминик
У меня не так много опыта работы с React, но вы можете определить свой собственный миксин с функциями, которые не перекрываются с пространством имен реальных объектов React. затем просто вызовите функции объекта "суперкласс" / композиции из ваших типичных функций компонентов React. тогда функции React не пересекаются с унаследованными функциями. это помогает уменьшить количество шаблонов, но ограничивает волшебство и упрощает работу React за кулисами. неужели это так сложно представить? Надеюсь, я ясно выразился.
Alexander Mills
Миксины никогда не умрут, потому что вы всегда можете просто сделать миксины своими руками. В React просто не будет «нативной» поддержки миксинов, но вы все равно можете делать миксины самостоятельно с помощью собственного JS.
Alexander Mills

Ответы:

109

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

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

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

  • Думая о React, я понял, что на самом деле мне не нужны для этого вложенные компоненты;
  • У Reusable Components есть отличный пример миксина.

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

ValidationMixin

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

Источник ( суть )

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

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

Источник ( суть )

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
@jmcejuela На самом деле, позже я перешел на более компонентный подход (все еще активно использую миксины), я мог бы расширить это в какой-то момент ..
Дэн Абрамов
1
Есть какие-нибудь новости о «более компонентном подходе»?
NilColor
3
@NilColor Пока нет, я не совсем доволен. :-) В настоящее время я FormInputразговариваю со своим владельцем через formLink. formLinkподобно valueLink, и возвращаются из FormMixin«S linkValidatedState(name, validator)методы. FormInputполучает свое значение от formLink.valueи вызывает formLink.requestBlurи formLink.requestFocus- они вызывают проверку в FormMixin. Наконец, чтобы настроить фактический компонент, используемый для ввода, я могу передать его FormInput:<FormInput component={React.DOM.textarea} ... />
Дэн Абрамов
Хороший ответ - несколько советов: вам не нужно вызывать donebluebird, и код будет работать, как в Q (или собственных обещаниях) - конечно, bluebird лучше. Также обратите внимание, что с момента ответа синтаксис в React изменился.
Бенджамин Грюнбаум
4

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

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

Миксины также часто используются во фреймворках, чтобы сделать API фреймворка доступным для всех компонентов с помощью «скрытой» контекстной функции React. Это больше не понадобится и с наследованием классов 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')} />;
  }
});

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

Себастьян Лорбер
источник
Ты прав. Фактически, документы LinkedStateMixin фактически объясняют, как это сделать без примеси. Этот конкретный миксин на самом деле просто немного синтаксического сахара.
nextgentech