javascript - Imposibil de accesat instanța React (this) în interiorul handler-ului de eveniment


Scriu un component simplu în ES6 (cu BabelJS), și funcțiile this.setState nu funcționează.

Erorile tipice includ ceva de genul

Nu se poate citi proprietatea 'setState' a unui obiect nedefinit

sau

this.setState nu este o funcție

Știi de ce? Iată codul:

import React from 'react'

class SomeClass extends React.Component {
  constructor(props) {
    super(props)
    this.state = {inputContent: 'startValue'}
  }

  sendContent(e) {
    console.log('sending input content '+React.findDOMNode(React.refs.someref).value)
  }

  changeContent(e) {
    this.setState({inputContent: e.target.value})
  } 

  render() {
    return (
      <div>
        <h4>The input form is here:</h4>
        Title: 
        <input type="text" ref="someref" value={this.inputContent} 
          onChange={this.changeContent} /> 
        <button onClick={this.sendContent}>Submit</button>
      </div>
    )
  }
}

export default SomeClass

Răspuns #1

this.changeContent trebuie să fie legat de instanța componentei prin this.changeContent.bind(this) înainte de a fi transmis ca prop onChange, altfel variabila this din corpul funcției nu va face referire la instanța componentei, ci la window. Vezi Function::bind.

Când folosiți React.createClass în loc de clase ES6, fiecare metodă non-lifecycle definită într-un component este automat legată de instanța componentului. Vezi Autobinding.

Fiți conștienți că legarea unei funcții creează o funcție nouă. Puteți să o legați direct în render, ceea ce înseamnă că o funcție nouă va fi creată de fiecare dată când componenta se redă, sau să o legați în constructorul dvs., ceea ce se va întâmpla o singură dată.

constructor() {
  this.changeContent = this.changeContent.bind(this);
}

vs

render() {
  return <input onChange={this.changeContent.bind(this)} />;
}

Referințele sunt setate pe instanța componentului și nu pe React.refs: trebuie să schimbați React.refs.someref în this.refs.someref. De asemenea, va trebui să legați metoda sendContent de instanța componentului astfel încât this să se refere la ea.

Răspunsul #2

Morhaus are dreptate, dar acest lucru poate fi rezolvat fără bind.

Poți folosi o funcție săgeată împreună cu propunerea proprietăților clasei:

class SomeClass extends React.Component {
  changeContent = (e) => {
    this.setState({inputContent: e.target.value})
  } 

  render() {
    return <input type="text" onChange={this.changeContent} />;
  }
}

Pentru că funcția săgeată este declarată în cadrul constructorului și pentru că funcțiile săgeată păstrează this din cadrul în care sunt declarate, totul funcționează. Dezavantajul aici este că acestea nu vor fi funcții pe prototip, ele vor fi recreate de fiecare dată cu fiecare componentă. Totuși, acesta nu este un mare dezavantaj, deoarece bind conduce la același rezultat.

Răspunsul #3

Această problemă este unul dintre primele lucruri pe care majoritatea dintre noi le experimentăm atunci când facem tranziția de la sintaxa de definire a componentelor React.createClass() la modul de clasa ES6 de a extinde React.Component.

Este cauzat de diferențele de context this în React.createClass() vs extends React.Component.

Folosirea React.createClass() va lega automat contextul this (valorile) corect, dar acest lucru nu se întâmplă când se folosesc clase ES6. Când se procedează în modul ES6 (prin extinderea React.Component), contextul this este null în mod implicit. Proprietățile clasei nu se leagă automat de instanța clasei React (componenta).

Abordări pentru a rezolva această problemă

Cunosc în total 4 abordări generale.

  1. Leagă-ți funcțiile în constructorul clasei. Considerată de mulți ca o abordare de bună practică care evită să atingi JSX-ul complet și nu creează o funcție nouă la fiecare re-redare a componentei.

    class SomeClass extends React.Component {
      constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
      }
      handleClick() {
        console.log(this); // the React Component instance
      }
      render() {
        return (
          <button onClick={this.handleClick}></button>
        );
      }
    }
    
  2. Leagă funcțiile tale inline. Poți încă găsi această abordare folosită aici și colo în unele tutoriale/articole/etc., așa că este important să fii conștient de ea. Este același concept ca la #1, dar fii conștient că legarea unei funcții creează o funcție nouă la fiecare re-redare.

    class SomeClass extends React.Component {
      handleClick() {
        console.log(this); // the React Component instance
      }
      render() {
        return (
          <button onClick={this.handleClick.bind(this)}></button>
        );
      }
    }
    
  3. Folosește o funcție săgeată cu săgeată groasă. Până la funcțiile săgeată, fiecare funcție nou definită avea propria valoare a lui this. Cu toate acestea, funcția săgeată nu creează propriul context this, așa că this are semnificația originală din instanța componentei React. Prin urmare, putem:

    class SomeClass extends React.Component {
      handleClick() {
        console.log(this); // the React Component instance
      }
      render() {
        return (
          <button onClick={ () => this.handleClick() }></button>
        );
      }
    }
    

    sau

    class SomeClass extends React.Component {
      handleClick = () => {
        console.log(this); // the React Component instance
      }
      render() {
        return (
          <button onClick={this.handleClick}></button>
        );
      }
    }
    
  4. Folosește biblioteca de funcții utilitare pentru a-ți lega funcțiile automat. Există câteva biblioteci utilitare care fac automat această treabă pentru tine. Iată câteva dintre cele populare, doar ca să menționăm câteva:

    • Decoratorul Autobind este un pachet NPM care leagă metodele unei clase de instanța corectă a this, chiar și atunci când metodele sunt detașate. Pachetul folosește @autobind înaintea metodelor pentru a lega this la referința corectă din contextul componentului.

      import autobind from 'autobind-decorator';
      
      class SomeClass extends React.Component {
        @autobind
        handleClick() {
          console.log(this); // the React Component instance
        }
        render() {
          return (
            <button onClick={this.handleClick}></button>
          );
        }
      }
      

      Decoratorul Autobind este suficient de inteligent încât să ne permită să legăm toate metodele dintr-o clasă de componentă dintr-o dată, exact ca abordarea #1.

    • Clasa Autobind este un alt pachet NPM care este utilizat pe scară largă pentru a rezolva această problemă de legare. Spre deosebire de Autobind Decorator, nu folosește modelul decorator, ci pur și simplu folosește o funcție în interiorul constructorului tău care leagă automat metodele Component la referința corectă a this.

      import autobind from 'class-autobind';
      
      class SomeClass extends React.Component {
        constructor() {
          autobind(this);
          // or if you want to bind only only select functions:
          // autobind(this, 'handleClick');
        }
        handleClick() {
          console.log(this); // the React Component instance
        }
        render() {
          return (
            <button onClick={this.handleClick}></button>
          );
        }
      }
      

      PS: O altă bibliotecă foarte asemănătoare este React Autobind.

Recomandare

Dacă aș fi în locul tău, aș rămâne la abordarea #1. Totuși, de îndată ce vei avea o mulțime de legări în constructorul clasei tale, ți-aș recomanda să explorezi una dintre bibliotecile de ajutor menționate în abordarea #4.

Altele

Nu este legat de problema pe care o ai, dar nu ar trebui să folosești prea des referințele.

Prima ta înclinație poate fi să folosești refs pentru a „face lucrurile să se întâmple” în aplicația ta. Dacă acesta este cazul, ia un moment și gândește-te mai critic despre unde ar trebui să fie deținut starea în ierarhia componentelor.

Pentru scopuri similare, exact ca cel de care ai nevoie, utilizarea unui component controlat este modul preferat. Îți sugerez să iei în considerare utilizarea state-ului componentului tău. Astfel, poți accesa pur și simplu valoarea astfel: this.state.inputContent.