React.js ES6 vermeidet es, 'this' an jede Methode zu binden

72

Vor kurzem habe ich angefangen, an React.js zu basteln, und ich liebe es. Ich habe mit dem regulären ES5 angefangen, um den Überblick zu behalten, sind die Dokumente alle in ES5 geschrieben ...

Aber jetzt wollte ich ES6 ausprobieren, weil es glänzend und neu ist und einige Dinge zu vereinfachen scheint. Was mich sehr stört, ist, dass ich für jede Methode, die ich meinen Komponentenklassen hinzugefügt habe, jetzt 'this' binden muss, sonst funktioniert es nicht. Mein Konstruktor sieht also so aus:

constructor(props) {
  super(props);
  this.state = { ...some initial state... }

  this.someHandler = this.someHandler.bind(this);
  this.someHandler = this.someHandler.bind(this);
  this.someHandler = this.someHandler.bind(this);
  this.someHandler = this.someHandler.bind(this);
  this.someHandler = this.someHandler.bind(this);
  this.someHandler = this.someHandler.bind(this);
  this.someHandler = this.someHandler.bind(this);
}

Wenn ich meiner Klasse noch mehr Methoden hinzufügen würde, würde dies zu einem noch größeren, hässlicheren Durcheinander werden.

Meine Frage ist, gibt es eine Möglichkeit, dies zu umgehen oder es zumindest einfacher, kürzer und weniger hässlich zu machen? Einer der Hauptgründe, warum ich React with ES6 ausprobieren wollte, war, meinen Code präziser zu gestalten, aber das Gegenteil ist der Fall. Anregungen oder Anregungen wäre dankbar.

Pavlin
quelle
1
Verwenden Sie wirklich alle Ihre Methoden irgendwo als Handler?
Bergi
Siehe auch diese Antwort
Bergi
Nun, dies wurde von einer übergeordneten Komponente übernommen, also ja, sie werden alle irgendwo in der Hierarchie verwendet. Ich habe versucht, ihrer Philosophie zu folgen und herauszufinden, welche Komponente was wissen muss, und das war es, was ich am Ende hatte.
Pavlin
Aber werden sie nicht als Methoden für das Komponentenobjekt aufgerufen (sollten nicht), anstatt als Funktionen weitergegeben zu werden?
Bergi
2
Ich persönlich verwende dafür ( speziellautobind ) Dekorateure anstelle von Klasseneigenschaftsinitialisierern.
Michelle Tilley

Antworten:

64

Sie können Klassenfelder verwenden , um die Bindung außerhalb des Konstruktors durchzuführen. Sie sehen wie folgt aus:

class Foo extends React.Component {

  handleBar = () => {
    console.log('neat');
  };

  handleFoo = () => {
    console.log('cool');
  };

  render() {
    return (
      <div
        onClick={this.handleBar}
        onMouseOver={this.handleFoo}
      />
    );
  }

}

Klassenfelder werden von Babel experimentell über seine Klasseneigenschaftstransformation unterstützt , aber sie sind immer noch "experimentell", da es sich um einen Entwurf der Stufe 3 handelt (noch nicht in einer Babel-Voreinstellung).

Sie müssen die Bindung jedoch manuell bis ES7 oder bis zur Aktivierung der Funktion in Babel durchführen. Dieses Thema wird in Babels Blog-Beitrag zu React on ES6 + kurz behandelt .

Ross Allen
quelle
Vielen Dank. Das ist eine Schande. Ich benutze gulp als Build-System und es scheint, als ob das gulp-Paket diese Option nicht hat. Irgendwelche Vorschläge, wie man das erträglicher macht?
Pavlin
1
Oh ja, also kannst du. Ich habe es mit laufen lassen babel({ stage: 0 })und es hat funktioniert. Dies scheint eine vorübergehende Lösung zu sein, da sie sich in den frühesten Stadien befindet, aber vorerst sehr gut funktionieren wird. Danke für deine Hilfe.
Pavlin
4
@Brandon Sie haben den gleichen Effekt. Die constructorMethode wird jedes Mal aufgerufen, wenn eine neue Instanz erstellt wird. Daher werden die bindAufrufe für jede Instanz ausgeführt. In den React-Dokumenten wird empfohlen, bindAufrufe in der renderFunktion zu vermeiden, da diese renderin einer bestimmten Instanz mehrmals aufgerufen werden, während der Konstruktor genau einmal aufgerufen wird.
Ross Allen
2
@DivyanshuMaithani Klassen-Eigenschaftsinitialisierer sind derzeit noch ein Vorschlag, daher kann nicht gesagt werden, ob die native Version die Leistung beeinträchtigt. Wenn Sie jedoch Babel verwenden, werden Eigenschaftsinitialisierer transpiliert, um gebundene Instanzen im Konstruktor zuzuweisen. sie sind genau die gleichen: babeljs.io/repl/…
Ross Allen
1
Dieses Beispiel war wirklich hilfreich, auch dieser Ansatz lässt die Komponente sauberer aussehen, wird
dazu passen
11

Eine andere Alternative ist die Verwendung von Dekorateuren. Sie deklarieren einen Getter für den Prototyp und definieren beim ersten Zugriff für eine Instanz eine eigene Eigenschaft mit einer gebundenen Version dieser Funktion.

Aber da ist ein Fang! In der Entwicklung wird die Eigenschaft nicht ersetzt, sondern bei jedem Zugriff gebunden. Dies bedeutet, dass Sie den React-Hot-Loader nicht unterbrechen . Zumindest für mich ist das ziemlich wichtig.

Ich habe eine Bibliothek erstellt, class-bind , die dies bereitstellt.

import {bound} from 'class-bind';

class App {
  constructor(){
    this.foo = 'bar';
  }

  @bound
  returnsFoo(){
    return this.foo;
  }

  render(){
    var returnsFoo = this.returnsFoo;
    return (
      <div>
        {returnsFoo()} === 'bar'
      </div>
    );
  }
}

Dekorateure zu instabil für Sie? Sie können alles oder einige Dinge mit den gleichen Vorteilen binden.

import {bind, bindAll} from 'class-bind';

bind(App.prototype, 'returnsFoo');

// or
bindAll(App.prototype);
Räuber
quelle
Das sieht ziemlich gut aus. Ich werde es auf jeden Fall versuchen. Ich habe mir den Hotloader noch nie wirklich angesehen, daher war das für mich kein Problem, aber es sieht sehr nützlich aus. Vielen Dank.
Pavlin
1

Ssorallens Vorschlag ist großartig, aber wenn Sie einen anderen Weg wollen, gibt es:

    class AppCtrlRender extends Component {
        binder(...methods) { methods.forEach( (method) => this[method] = this[method].bind(this) ); }

        render() {
            var isMobile = this.state.appData.isMobile;
            var messages = this.state.appData.messages;
            return (
                <div id='AppCtrlSty' style={AppCtrlSty}>
                    React 1.3 Slider
                    <br/><br/>
                    <div className='FlexBoxWrap'>
                        <Slider isMobile={isMobile}/>
                        <JList data={messages}/>
                    </div>
                </div>
            );
        }
    }

    var getAppState = function() {
        return {
            appData: AppStore.getAppData()
        };
    };

    export default class AppCtrl extends AppCtrlRender {
        constructor() {
            super();
            this.state = getAppState();
            this.binder('appStoreDidChange');
        }

        componentDidMount() {
            var navPlatform = window.navigator.platform;
            Actions.setWindowDefaults(navPlatform);
        }
        componentWillMount() { AppStore.onAny(this.appStoreDidChange); }
        componentWillUnmount() { AppStore.offAny(this.appStoreDidChange); }
        appStoreDidChange() { this.setState(getAppState()); }
    }

Sie können this.binder beliebig viele Methoden hinzufügen ('method1', 'method2', ...)

J. Mark Stevens
quelle
Vielen Dank für den Vorschlag, dies würde die Bindungsmethoden weniger schmerzhaft machen, aber ich muss trotzdem angeben, welche Methoden gebunden werden sollen. Zur Zeit funktioniert der Vorschlag von ssorallens einwandfrei. Ich habe nur experimentelle Funktionen für babel aktiviert. Wie lange dies eine praktikable Option sein wird, weiß ich nicht, aber im Moment denke ich, dass es die kürzeste und sauberste Option ist.
Pavlin
Ich habe die Bindemittelmethode getestet und sie funktioniert hervorragend. Ich habe die Methode gerade in umbenannt bindPublicMethods, sodass sie einen informativen Nutzen bringt, da es in es6 keine Möglichkeit gibt, öffentliche und private Methoden zu markieren.
Trendfischer
1

Wenn Sie verwenden, stage-0gibt es eine Funktionsbindungssyntax.

class MyComp extends Component {

  handleClick() { console.log('doing things') }

  render() {
    return <button onClick={::this.handleClick}>Do Things</button>
  }

}

Dies zerstört this.handleClick.call(this), was ich im Allgemeinen für performant genug halte.

Jon Jaques
quelle
3
Wow, das ist ziemlich gut, aber da es sich in Phase 0 befindet, würde ich wahrscheinlich nicht empfehlen, es für irgendetwas Ernstes zu verwenden. Mir war nicht bewusst, dass sie diese Funktion vorgeschlagen haben.
Pavlin
Auf jeden Fall ein solider Rat, aber ich gebe zu, dass ich mir darüber keine allzu großen Sorgen mache und ihn großzügig benutze: D
Jon Jaques
4
Die React-Dokumente raten davon ab, da das Einfügen einer Bindemethode in die renderMethode einer Komponente das Binden bei jedem erneuten Rendern erzwingt. " Wir empfehlen, dass Sie Ihre Ereignishandler im Konstruktor binden, damit sie für jede Instanz nur einmal gebunden werden " Quelle
Brandon
Wie wäre es mit einfachen <button onClick = {() => this.handleClick ()}> Do Things </ button>?
Thomaszbak
Es sollte Zucker für<button onClick={() => this.handleClick(arguments)}>
Daviestar
1

Eine Idee, um ein Binden zu vermeiden

class MyComp extends Component {

  render() {
    return <button onClick={e => this.handleClick(e)}>Do Things</button>
  }

}

Haftungsausschluss: Ungetestet kann auch nicht einfach mehr als ein Argument verarbeiten (in diesem Fall gibt es ein Ereignis (e).

Auch diese Antwort ist wahrscheinlich ein Beispiel dafür, was nicht zu tun ist, laut diesem Artikel, der wahrscheinlich eine lohnende Lektüre ist:

https://daveceddia.com/avoid-bind-when-passing-props/

Alexander Mills
quelle
onClick = {(e, arg1, arg2, arg3) => this.handleClick(e, arg1, arg2, arg3)}hätte 4 Argumente. Pfeilfunktionen können beliebig viele Argumente verwenden.
Brandon Dube
2
Auf diese Weise wird jedes Mal, wenn Render aufgerufen wird, eine neue Funktion erstellt
ciekawy
1

Eigentlich ziehe ich es vor, die OOP-Vererbung zu imitieren, indem ich Kindern den Elternkontext übergebe.

class Parent extends Component {
  state = {happy: false}

  changeState(happy) {
    this.setState({happy})
  }

  render() {
    return (
      <Child parent={this} >
    )
  }
}

class Child extends Component {
   //...
   this.props.parent.changeState(true)
}

$ 0,02, Jon

Jon Pellant
quelle
1

Ich habe eine Methode erstellt, um alle "Bindungen" zu organisieren.

class MyClass {
  constructor() {

    this.bindMethods([
      'updateLocationFields',
      'render',
      'loadCities',
    ]);
  }

  bindMethods(methods) {
    methods.forEach((item) => {
      this[item] = this[item].bind(this);
    });
  }

  ...
}
Pablo Darde
quelle
>.>. . . . . .
Andrew
0

Ich benutze eine Hilfsfunktion doBinding(this), die ich in jedem Konstruktor aufrufe. In diesem Beispiel bindet es _handleChange1()und _handleChange2().

class NameForm extends React.Component {
    constructor(props) {
        super(props);
        doBinding(this);
        this.state = {value1: "", value2: ""};
    }
    _handleChange1(event) {
        this.setState({value1: event.target.value});
    }
    _handleChange2(event) {
        this.setState({value2: event.target.value});
    }
    render() {
       ...
    }
}

Die Methode funktioniert auch, wenn Sie Babel nicht verwenden.

Meine Handler-Methoden beginnen alle mit _(eine Konvention, die angibt, dass sie privat sind). Also doBinding()sucht nach dem _. Sie können das entfernen, if (key.startsWith("_"))wenn Sie diese Konvention nicht verwenden.

function doBinding(obj) {
    const proto = Object.getPrototypeOf(obj);
    for (const key of Object.getOwnPropertyNames(proto)) {
        if (key.startsWith("_")) {
            obj[key] = obj[key].bind(obj);
        }
    }
}
James
quelle
0

Wie wäre es mit einer gemeinsamen Funktion, um die Bindungsarbeit wie folgt auszuführen:

// common function:
function bind(self,methods){
     for(var key in methods){
       self[key] = methods[key].bind(self);
     }
}

// your class:
class MyClass {
     constructor() {
          bind(this,{
              someHandler1(event){ 
                //... 
              },
              someHandler2(event){ 
                //...
              }
          })
     }
}
MaxP
quelle