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! globalNo 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! globalVí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();
// SaurabhAgora, 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 undefinedNã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(); // SaurabhNã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(); // SaurabhO 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.