web-dev-qa-db-fra.com

React.js ES6 évite de lier 'this' à chaque méthode

Récemment, j'ai commencé à bricoler avec React.js et j'adore ça. J'ai commencé dans l'ES5 classique, pour que tout soit clair, les documents sont tous écrits en ES5 ...

Mais maintenant, je voulais essayer ES6, car il est brillant et nouveau, et cela semble simplifier certaines choses. Ce qui me dérange beaucoup, c'est que pour chaque méthode que j'ai ajoutée à mes classes de composants, je dois maintenant lier 'this' à, sinon cela ne fonctionne pas. Alors mon constructeur finit par ressembler à ceci:

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);
}

Si je devais ajouter encore plus de méthodes à ma classe, cela deviendrait un gâchis encore plus grand et plus laid.

Ma question est la suivante: y a-t-il un moyen de contourner cela, ou au moins de le rendre plus facile, plus court et moins laid? L'une des principales raisons pour lesquelles je voulais essayer React avec ES6 était de rendre mon code plus concis, mais c'est l'inverse. Toute suggestion ou contribution serait appréciée.

60
Pavlin

Vous pouvez utiliser class fields pour effectuer la liaison en dehors du constructeur. Ils ressemblent à ceci:

class Foo extends React.Component {

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

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

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

}

Les champs de classe sont supportés expérimentalement par Babel via sa transformation de propriétés class , mais ils sont toujours "expérimentaux" car il s'agit d'un Stage 3 Draft (pas encore dans un préréglage Babel).

Vous devrez toutefois faire la liaison manuellement jusqu'à ES7 ou jusqu'à l'activation de la fonctionnalité dans Babel. Ce sujet est brièvement abordé dans le billet de blog de Babel sur React on ES6 + .

51
Ross Allen

Une autre alternative consiste à utiliser des décorateurs. Vous déclarez un getter sur le prototype et, lors du premier accès à une instance, il définit une propriété propre avec une version liée de cette fonction.

Mais il y a un piège! En développement, il ne remplacera pas la propriété, il se liera à chaque accès. Cela signifie que vous ne cassez pas react-hot-loader. Au moins pour moi, c'est assez important.

J'ai créé une bibliothèque, class-bind , qui fournit cela.

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>
    );
  }
}

Les décorateurs sont trop instables pour vous? Vous pouvez lier tout ou certaines choses avec les mêmes avantages.

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

bind(App.prototype, 'returnsFoo');

// or
bindAll(App.prototype);
11
FakeRainBrigand

Une idée à éviter

class MyComp extends Component {

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

}

disclaimer: non testé, également, ne peut pas facilement gérer plus d'un argument (dans ce cas, il y en a un, l'événement (e).

En outre, cette réponse est probablement un exemple de ce que not doit faire, selon cet article qui vaut probablement la peine d'être lu:

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

1
Alexander Mills

La suggestion de Ssorallen est excellente, mais si vous voulez une autre façon, il y a:

    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()); }
    }

Vous pouvez ajouter autant de méthodes que vous voulez dans this.binder ('method1', 'method2', ...)

1
J. Mark Stevens

Si vous utilisez stage-0, il existe une syntaxe de liaison de fonction.

class MyComp extends Component {

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

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

}

Cette destructure à this.handleClick.call(this), que je pense est généralement assez performant.

1
Jon Jaques

J'ai créé une méthode pour organiser tous les "liens". 

class MyClass {
  constructor() {

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

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

  ...
}
0
Pablo Darde

En fait, je préfère imiter OOP en transmettant aux enfants le contexte parent.

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

0
Jon Pellant

J'utilise une fonction d'assistancedoBinding(this), que j'appelle dans chaque constructeur. Dans cet exemple, il lie _handleChange1() et _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() {
       ...
    }
}

La méthode fonctionne même si vous n'utilisez pas Babel.

Mes méthodes de gestion commencent toutes par _ (une convention indiquant qu'elles sont privées). Donc, doBinding() cherche le _. Vous pouvez supprimer la if (key.startsWith("_")) si vous n'utilisez pas cette convention.

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