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.