Artigo original: This is why we need to bind event handlers in Class Components in React

Enquanto trabalhava com React, você provavelmente encontrou componentes controlados e manipuladores de eventos. Ao usar esses métodos, precisamos vinculá-los à instância dos componentes utilizando .bind() no construtor do nosso componente customizado.

class Foo extends React.Component{
  constructor( props ){
    super( props );
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick(event){
    // sua lógica de manipulação do evento
  }
  
  render(){
    return (
      <button type="button" 
      onClick={this.handleClick}>
      Clique aqui
      </button>
    );
  }
}

ReactDOM.render(
  <Foo />,
  document.getElementById("app")
);

Neste artigo, vamos descobrir por que precisamos fazer isso.

Se você ainda não conhece a função do .bind(), eu recomendo que você leia aqui o que ele faz.

Culpa do JavaScript, não do React

Bem, colocar a culpa soa um pouco duro. Isso, porém, não é algo que precisemos fazer por causa da maneira como o React funciona ou por causa do JSX. Isso acontece pela forma como o vínculo de this funciona em JavaScript.

Vejamos o que acontece se não vincularmos o manipulador de evento à instância do componente:

class Foo extends React.Component{
  constructor( props ){
    super( props );
  }
    
  handleClick(event){
    console.log(this); // 'this' é undefined
  }
    
  render(){
    return (
      <button type="button" onClick={this.handleClick}>
        Clique aqui
      </button>
    );
  }
}

ReactDOM.render(
  <Foo />,
  document.getElementById("app")
);


Ao executar esse código, clique no botão "Clique aqui" e verifique seu console. Você verá  undefined impresso no console como valor do this que está dentro do manipulador de eventos. O método handleClick() parece ter perdido seu contexto (a instância do componente) ou o valor do this.

Como funciona vincular o "this" em JavaScript

Como já mencionei, isso acontece pelo modo como o vínculo do this funciona em JavaScript. Não entrarei em detalhes neste artigo, mas aqui tem um ótimo material para entender o funcionamento do vínculo do this em JavaScript (texto em inglês).

O relevante para nossa discussão aqui é como o valor do this em uma função depende de como essa função é invocada.

Vínculo padrão

function display(){
 console.log(this); // esse 'this' aponta para o objeto global
}

display(); 

Esta é uma chamada de função simples. O valor do this no método display(), neste caso, é o objeto window — ou global — no modo não estrito. No modo estrito o valor de this é undefined.

Vínculo implícito

var obj = {
 name: 'Saurabh',
 display: function(){
   console.log(this.name); // esse 'this' aponta para obj
  }
};

obj.display(); // Saurabh 

Quando chamamos a função desse modo — inserida em um objeto — o valor do this no display() faz referência ao obj.

Quando, no entanto, atribuímos essa referência de função em outra variável e invocamos a função usando essa nova referência de função, obteremos um valor diferente do this no display() .

var name = "uh oh! global";
var outerDisplay = obj.display;
outerDisplay(); // uh oh! global

No exemplo acima, quando chamamos outerDisplay(), não especificamos o objeto. Essa é uma chamada de função simples sem o objeto proprietário. Nesse caso, o valor do this dentro do display() volta para o vínculo padrão; ele aponta para o objeto global ou retorna undefined, se a função for chamada usando o modo estrito.

Isso é aplicável especialmente ao passar essas funções como callbacks em outras funções customizadas, funções de bibliotecas externas ou funções integradas ao JavaScript, como setTimeout .

Considere que a implementação fictícia abaixo é a definição do setTimeout e chame-a.

// Implementação fictícia do setTimeout
function setTimeout(callback, atraso){

   //'atraso' em milissegundos
   callback();
   
}

setTimeout( obj.display, 1000 );

Podemos ver que, ao chamar setTimeout, o JavaScript atribui internamente obj.display como callback .

callback = obj.display;

Essa operação de atribuição, como visto anteriormente, faz com que a função display() perca seu contexto. Quando essa função de callback finalmente for chamada dentro de setTimeout, o valor do this dentro do display() volta para o vínculo padrão.

var name = "uh oh! global";
setTimeout( obj.display, 1000 );

// uh oh! global

Vínculo inflexível explícito

Para evitar isso, podemos vincular explicitamente o valor de this a uma função usando o método bind().

var name = "uh oh! global";
obj.display = obj.display.bind(obj); 
var outerDisplay = obj.display;
outerDisplay();

// Saurabh

Agora, quando chamamos outerDisplay(), o valor de this aponta para o obj que está dentro de display() .

Ainda que passemos obj.display como uma callback, o valor do this dentro de display() apontará corretamente para obj .

Recriando o cenário usando apenas JavaScript

No início deste artigo, vimos esse comportamento em nosso componente do React chamado Foo . Se não vinculássemos o manipulador de eventos com this , seu valor dentro dele seria undefined.

Como expliquei acima, isso acontece pelo modo como o vínculo de this funciona no JavaScript e não está relacionado ao modo como o React funciona. Para visualizar isso, vamos remover o código específico do React e construir um exemplo semelhante com JavaScript puro simulando esse comportamento.

class Foo {
  constructor(name){
    this.name = name
  }
  
  display(){
    console.log(this.name);
  }
}

var foo = new Foo('Saurabh');
foo.display(); // Saurabh

// A operação de atribuição a seguir simula uma perda de contexto
// semelhante a que ocorre ao passar o manipulador como callback 
// em um componente do React real
var display = foo.display; 
display(); // TypeError: this is undefined

Não estamos simulando eventos e manipuladores reais, mas usando um código sinônimo. Conforme observamos no exemplo do componente do React, o valor de this era undefined porque o contexto foi perdido depois de passar o manipulador como callback — sinônimo a uma operação de atribuição. Isso é o que observamos também neste fragmento de código JavaScript que não é do React.

“Espere um minuto! O valor de this não deveria apontar para o objeto global, considerando que estamos executando isso no modo não estrito e de acordo com as regras de vinculação padrão? ” você poderia perguntar.

Não. O motivo é o seguinte:

Os corpos das declarações e expressões de classe são executados em modo estrito, ou seja, os métodos constructor, static e prototype; as funções getter e setter são executadas no modo estrito.

Você pode ler o artigo completo aqui.

Então, para evitar o erro, precisamos vincular o valor de this assim:

class Foo {
  constructor(name){
    this.name = name
    this.display = this.display.bind(this);
  }
  
  display(){
    console.log(this.name);
  }
}

var foo = new Foo('Saurabh');
foo.display(); // Saurabh

var display = foo.display;
display(); // Saurabh

Não precisamos fazer isso no construtor. Podemos fazer isso em outro lugar também. Considere o seguinte:

class Foo {
  constructor(name){
    this.name = name;
  }
  display(){
    console.log(this.name);
  }
}

var foo = new Foo('Saurabh');
foo.display = foo.display.bind(foo);
foo.display(); // Saurabh

var display = foo.display;
display(); // Saurabh

O construtor, porém, é realmente o melhor e mais eficiente lugar do código para incluir as instruções de vinculação do manipulador de eventos, considerando que é onde ocorre toda a inicialização.

Por que não precisamos vincular o this nas arrow functions?

Temos mais duas maneiras de definir manipuladores de eventos dentro de um componente do React.

class Foo extends React.Component{
  handleClick = () => {
    console.log(this); 
  }
 
  render(){
    return (
      <button type="button" onClick={this.handleClick}>
        Clique aqui
      </button>
    );
  }
} 

ReactDOM.render(
  <Foo />,
  document.getElementById("app")
);
class Foo extends React.Component{
 handleClick(event){
    console.log(this);
  }
 
  render(){
    return (
      <button type="button" onClick={(e) => this.handleClick(e)}>
        Clique aqui
      </button>
    );
  }
}

ReactDOM.render(
  <Foo />,
  document.getElementById("app")
);

Ambos usam arrow functions, introduzidas no ES6. Quando optamos por essa alternativa, nosso manipulador de eventos já está automaticamente vinculado à instância do componente. Então, não precisamos fazer o vínculo no construtor.

O motivo disso é que, no caso das arrow functions, o this é vinculado de modo lexical. Isso significa que o contexto da função que o envolve – ou o escopo global – é usado como valor do this.

No exemplo da sintaxe dos campos públicos da classe, a arrow function está fechada dentro da classe Foo  — ou função construtora. Assim, o contexto é a instância do componente, que é o que queremos.

No exemplo da arrow function como callback, a arrow function está fechada dentro do método render(), que é invocado pelo React no contexto da instância do componente. É por isso que a arrow function também possuirá esse contexto. O valor do this nela apontará corretamente para a instância do componente.

Para mais detalhes sobre a léxico do vínculo this, confira este excelente recurso.

Para encurtar uma longa história

Em componentes de classe do React, quando passamos a referência da função de manipulação de eventos como callback, conforme abaixo

<button type="button" onClick={this.handleClick}>Clique aqui</button>

o manipulador de eventos perde seu contexto de vínculo implícito. Quando o evento ocorre e o manipulador é chamado, o valor do this volta para o vínculo padrão e é definido como undefined, visto que as declarações de classe e os métodos protótipos são executados no modo estrito.

Quando vinculamos o this do manipulador de eventos à instância do componente no construtor, podemos passá-lo como callback sem preocupação com a perda de seu contexto.

Arrow functions estão isentas desse comportamento porque usam vínculo lexical com this, o que as vincula automaticamente ao escopo em que são definidas.