
Mientras trabajaba en React, debe haber encontrado componentes controlados y controladores de eventos. Necesitamos vincular estos métodos a la instancia del componente usando .bind()
en el constructor de nuestro componente personalizado.
class Foo extends React.Component{ constructor( props ){ super( props ); this.handleClick = this.handleClick.bind(this); } handleClick(event){ // your event handling logic } render(){ return ( Click Me ); } } ReactDOM.render( , document.getElementById("app") );
En este artículo, vamos a averiguar por qué necesitamos hacer esto.
Recomendaría leer acerca de .bind()
aquí si aún no sabe lo que hace.
Culpar a JavaScript, no reaccionar
Bueno, echar la culpa suena un poco duro. Esto no es algo que debamos hacer debido a la forma en que funciona React o debido a JSX. Esto se debe a la forma en que funciona el this
enlace en JavaScript.
Veamos qué sucede si no vinculamos el método del controlador de eventos con su instancia de componente:
class Foo extends React.Component{ constructor( props ){ super( props ); } handleClick(event){ console.log(this); // 'this' is undefined } render(){ return ( Click Me ); } } ReactDOM.render( , document.getElementById("app") );
Si ejecuta este código, haga clic en el botón "Haga clic en mí" y verifique su consola. Verá undefined
impreso en la consola como el valor de this
dentro del método del controlador de eventos. El handleClick()
método parece haber perdido su contexto (instancia de componente) o this
valor.
Cómo funciona 'este' enlace en JavaScript
Como mencioné, esto sucede debido a la forma en this
que funciona el enlace en JavaScript. No entraré en muchos detalles en esta publicación, pero aquí hay un gran recurso para comprender cómo funciona el this
enlace en JavaScript.
Pero relevante para nuestra discusión aquí, el valor de this
dentro de una función depende de cómo se invoca esa función.
Enlace predeterminado
function display(){ console.log(this); // 'this' will point to the global object } display();
Esta es una llamada de función simple. El valor de this
dentro del display()
método en este caso es la ventana, o el objeto global, en modo no estricto. En modo estricto, el this
valor es undefined
.
Enlace implícito
var obj = { name: 'Saurabh', display: function(){ console.log(this.name); // 'this' points to obj } }; obj.display(); // Saurabh
Cuando llamamos a una función de esta manera, precedida por un objeto de contexto, el this
valor interno display()
se establece en obj
.
Pero cuando asignamos esta referencia de función a alguna otra variable e invocamos la función usando esta nueva referencia de función, obtenemos un valor diferente de this
inside display()
.
var name = "uh oh! global"; var outerDisplay = obj.display; outerDisplay(); // uh oh! global
En el ejemplo anterior, cuando llamamos outerDisplay()
, no especificamos un objeto de contexto. Es una llamada de función simple sin un objeto propietario. En este caso, el valor de this
inside display()
vuelve al enlace predeterminado . Apunta al objeto global o undefined
si la función que se invoca utiliza el modo estricto.
Esto es especialmente aplicable al pasar funciones como devoluciones de llamada a otra función personalizada, una función de biblioteca de terceros o una función de JavaScript incorporada como setTimeout
.
Considere la setTimeout
definición ficticia como se muestra a continuación y luego invocala.
// A dummy implementation of setTimeout function setTimeout(callback, delay){ //wait for 'delay' milliseconds callback(); } setTimeout( obj.display, 1000 );
Podemos descubrir que cuando llamamos setTimeout
, JavaScript asigna internamente obj.display
a su argumento callback
.
callback = obj.display;
Esta operación de asignación, como hemos visto antes, hace que la display()
función pierda su contexto. Cuando esta devolución de llamada se invoca finalmente en el interior setTimeout
, el this
valor del interior display()
vuelve al enlace predeterminado .
var name = "uh oh! global"; setTimeout( obj.display, 1000 ); // uh oh! global
Enlace duro explícito
Para evitar esto, podemos vincular explícitamente el this
valor a una función mediante el bind()
método.
var name = "uh oh! global"; obj.display = obj.display.bind(obj); var outerDisplay = obj.display; outerDisplay(); // Saurabh
Ahora, cuando llamamos outerDisplay()
, el valor de this
apunta hacia obj
adentro display()
.
Incluso si pasamos obj.display
como una devolución de llamada, el this
valor dentro display()
apuntará correctamente obj
.
Recreando el escenario usando solo JavaScript
Al comienzo de este artículo, vimos esto en nuestro componente React llamado Foo
. Si no vinculamos el controlador de eventos con this
, su valor dentro del controlador de eventos se estableció como undefined
.
Como mencioné y expliqué, esto se debe a la forma en this
que funciona el enlace en JavaScript y no está relacionado con cómo funciona React. Así que eliminemos el código específico de React y construyamos un ejemplo de JavaScript puro similar para simular este comportamiento.
class Foo { constructor(name){ this.name = name } display(){ console.log(this.name); } } var foo = new Foo('Saurabh'); foo.display(); // Saurabh // The assignment operation below simulates loss of context // similar to passing the handler as a callback in the actual // React Component var display = foo.display; display(); // TypeError: this is undefined
No estamos simulando eventos y controladores reales, sino que estamos usando un código sinónimo. Como observamos en el ejemplo del componente React, el this
valor fue undefined
como el contexto se perdió después de pasar el controlador como una devolución de llamada, sinónimo de una operación de asignación. Esto es lo que observamos aquí también en este fragmento de JavaScript que no es de React.
"¡Espera un minuto! ¿No debería el this
valor apuntar al objeto global, ya que lo estamos ejecutando en modo no estricto de acuerdo con las reglas del enlace predeterminado? " podría preguntar.
No. Por eso:
Los cuerpos de las declaraciones de clase y las expresiones de clase se ejecutan en modo estricto, es decir, los métodos constructor, estático y prototipo. Las funciones getter y setter se ejecutan en modo estricto.Puedes leer el articulo completo aquí.
Entonces, para evitar el error, necesitamos vincular el this
valor de esta manera:
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
No necesitamos hacer esto en el constructor, y también podemos hacerlo en otro lugar. Considera esto:
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
But the constructor is the most optimal and efficient place to code our event handler bind statements, considering that this is where all the initialization takes place.
Why don’t we need to bind ‘this’
for Arrow functions?
We have two more ways we can define event handlers inside a React component.
- Public Class Fields Syntax(Experimental)
class Foo extends React.Component{ handleClick = () => { console.log(this); } render(){ return ( Click Me ); } } ReactDOM.render( , document.getElementById("app") );
- Arrow function in the callback
class Foo extends React.Component{ handleClick(event){ console.log(this); } render(){ return ( this.handleClick(e)}> Click Me ); } } ReactDOM.render( , document.getElementById("app") );
Both of these use the arrow functions introduced in ES6. When using these alternatives, our event handler is already automatically bound to the component instance, and we do not need to bind it in the constructor.
The reason is that in the case of arrow functions, this
is bound lexically. This means that it uses the context of the enclosing function — or global — scope as its this
value.
In the case of the public class fields syntax example, the arrow function is enclosed inside the Foo
class — or constructor function — so the context is the component instance, which is what we want.
In the case of the arrow function as callback example, the arrow function is enclosed inside the render()
method, which is invoked by React in the context of the component instance. This is why the arrow function will also capture this same context, and the this
value inside it will properly point to the component instance.
For more details regarding lexical this
binding, check out this excellent resource.
To make a long story short
In Class Components in React, when we pass the event handler function reference as a callback like this
Click Me
the event handler method loses its implicitly bound context. When the event occurs and the handler is invoked, the this
value falls back to default binding and is set to undefined
, as class declarations and prototype methods run in strict mode.
When we bind the this
of the event handler to the component instance in the constructor, we can pass it as a callback without worrying about it losing its context.
Arrow functions are exempt from this behavior because they use lexicalthis
binding which automatically binds them to the scope they are defined in.