Używanie mixinów i komponentów do ponownego wykorzystania kodu w Facebook React

116

Zaczynam używać Facebook React w projekcie Backbone i na razie wszystko idzie naprawdę dobrze.
Zauważyłem jednak, że do mojego kodu React wkradło się pewne duplikowanie.

Na przykład mam kilka widżetów przypominających formularze ze stanami takimi jak INITIAL, SENDINGi SENT. Po naciśnięciu przycisku formularz musi zostać zweryfikowany, zostanie wysłane żądanie, a następnie stan zostanie zaktualizowany. Stan jest this.stateoczywiście przechowywany w React wraz z wartościami pól.

Gdyby to były widoki Backbone, wyodrębniłbym klasę bazową o nazwie, FormViewale mam wrażenie, że React nie wspiera ani nie obsługuje podklas w celu udostępnienia logiki widoku (popraw mnie, jeśli się mylę).

Widziałem dwa podejścia do ponownego wykorzystania kodu w Reakcie:

Czy mam rację, że mixiny i kontenery są preferowane zamiast dziedziczenia w Reakcie? Czy to przemyślana decyzja projektowa? Czy bardziej sensowne byłoby użycie komponentu mixin lub kontenera dla mojego przykładu „widżetu formularza” z drugiego akapitu?

Oto istota z FeedbackWidgeti JoinWidgetw ich aktualnym stanie . Mają podobną strukturę, podobną beginSendmetodę i oba będą wymagały wsparcia walidacji (jeszcze nie ma).

Dan Abramov
źródło
W ramach aktualizacji - reaguj, zastanawiasz się nad wspieraniem miksów w długiej przyszłości, ponieważ kiedy np. Wszystkie twoje komponenty componentDidMount działają magicznie, reaguj na skomplikowane rzeczy, więc nie nadpisują się nawzajem ... ponieważ miksy są bardzo uproszczone i nie nadające się do celu
Dominic
Nie mam zbyt dużego doświadczenia z Reactem, ale możesz zdefiniować swój własny miks z funkcjami, które nie pokrywają się z przestrzenią nazw rzeczywistych obiektów React. następnie po prostu wywołaj funkcje obiektu "nadklasy" / kompozycji z typowych funkcji komponentów Reacta. wtedy nie ma nakładania się funkcji React i funkcji dziedziczonych. pomaga to zredukować pewne schematy, ale ogranicza magię, która ma miejsce i ułatwia samemu Reactowi działanie za kulisami. czy to naprawdę takie trudne do wyobrażenia? Mam nadzieję, że wyraziłem się jasno.
Alexander Mills,
Miksy nigdy nie umrą, ponieważ zawsze możesz po prostu zrobić miksy DIY. React po prostu nie będzie miał "natywnej" obsługi miksów, ale nadal możesz samodzielnie miksować za pomocą natywnego JS.
Alexander Mills,

Odpowiedzi:

109

Aktualizacja: ta odpowiedź jest nieaktualna. Trzymaj się z dala od miksów, jeśli możesz. Ostrzegałem was!
Mixiny są martwe. Niech żyje kompozycja

Na początku próbowałem użyć do tego podkomponentów i wyodrębnić FormWidgeti InputWidget. Jednak porzuciłem to podejście w połowie, ponieważ chciałem mieć lepszą kontrolę nad generowanymi inputi ich stanem.

Dwa artykuły, które najbardziej mi pomogły:

Okazało się, że wystarczyło napisać dwa (różne) miksy: ValidationMixini FormMixin.
Oto jak je rozdzieliłem.

ValidationMixin

Mixin walidacji dodaje wygodne metody do uruchamiania funkcji walidatora na niektórych właściwościach stanu i przechowuje właściwości „błędne” w state.errorstablicy, dzięki czemu można podświetlić odpowiednie pola.

Źródło ( treść )

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;

});

Stosowanie

ValidationMixinma trzy metody: validate, hasErrori resetError.
Oczekuje, że klasa zdefiniuje validatorsobiekt, podobnie jak propTypes:

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

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

  // ...

});

Gdy użytkownik naciska przycisk przesyłania, dzwonię validate. Wywołanie validatespowoduje uruchomienie każdego walidatora i wypełnienie this.state.errorstablicą zawierającą klucze właściwości, które nie przeszły weryfikacji.

W mojej rendermetodzie używam hasErrordo generowania poprawnej klasy CSS dla pól. Kiedy użytkownik umieszcza fokus wewnątrz pola, wzywam, resetErroraby usunąć podświetlenie błędu do następnego validatewywołania.

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

Forma mixin obsługuje stan formularza (edytowalny, przesyłający, przesłany). Możesz go użyć do wyłączenia wejść i przycisków podczas wysyłania żądania i odpowiednio zaktualizować widok, gdy zostanie wysłany.

Źródło ( treść )

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;

});

Stosowanie

Oczekuje, że komponent zapewni jedną metodę: sendRequest :, która powinna zwrócić obietnicę Bluebird. (Zmodyfikowanie go do pracy z Q lub inną biblioteką obietnic jest trywialne).

Zapewnia wygodę metod, takich jak isFormEditable, isFormSubmittingi isFormSubmitted. Zapewnia on również sposobu skopać żądanie: submitForm. Możesz to wywołać z programu onClickobsługi przycisków formularza .

Dan Abramov
źródło
2
@jmcejuela W rzeczywistości przeniosłem się do bardziej komponentowego podejścia później (nadal intensywnie używam mixinów), mogę to rozwinąć w pewnym momencie ..
Dan Abramov
1
Czy są jakieś wiadomości na temat „podejścia bardziej komponentowego”?
NilColor,
3
@NilColor Jeszcze nie, nie jestem z tego zadowolony. :-) Obecnie FormInputrozmawiam ze swoim właścicielem przez formLink. formLinkjest jak valueLink, i jest zawracany z FormMixin„s linkValidatedState(name, validator)metody. FormInputpobiera swoją wartość z formLink.valuei wywołuje formLink.requestBluri formLink.requestFocus- powodują walidację w FormMixin. Na koniec, aby dostosować rzeczywisty komponent używany do wprowadzania danych, mogę przekazać go FormInput:<FormInput component={React.DOM.textarea} ... />
Dan Abramov,
Dobra odpowiedź - kilka wskazówek: nie musisz dzwonić donedo bluebirda, a kod będzie działał jak w Q (lub natywne obietnice) - oczywiście bluebird jest lepszy. Zauważ również, że składnia zmieniła się w Reakcie od czasu odpowiedzi.
Benjamin Gruenbaum
4

Tworzę SPA z Reactem (w produkcji od 1 roku) i prawie nigdy nie używam mixinów.

Jedyny przypadek, jaki mam obecnie w przypadku mixinów, to udostępnianie zachowań wykorzystujących metody cyklu życia Reacta ( componentDidMountitp.). Ten problem został rozwiązany przez komponenty wyższego rzędu, o których Dan Abramov mówi w swoim łączu (lub za pomocą dziedziczenia klasy ES6).

Miksery są również często używane w frameworkach, aby udostępnić API frameworka wszystkim komponentom, używając "ukrytego" kontekstu Reacta. Nie będzie to już potrzebne w przypadku dziedziczenia klas ES6.


W większości innych przypadków, miksy są używane, ale tak naprawdę nie są potrzebne i można je łatwiej zastąpić prostymi pomocnikami.

Na przykład:

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

Możesz bardzo łatwo refaktoryzować LinkedStateMixinkod, tak aby składnia wyglądała następująco:

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

Czy jest jakaś duża różnica?

Sebastien Lorber
źródło
Masz rację. W rzeczywistości dokumentacja LinkedStateMixin faktycznie opisuje, jak to zrobić bez miksera. Ta konkretna mieszanka to tak naprawdę tylko mały cukier syntaktyczny.
nextgentech