Esta es la razón por la que necesitamos vincular controladores de eventos en Class Components en React

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 thisenlace 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á undefinedimpreso en la consola como el valor de thisdentro del método del controlador de eventos. El handleClick()método parece haber perdido su contexto (instancia de componente) o thisvalor.

Cómo funciona 'este' enlace en JavaScript

Como mencioné, esto sucede debido a la forma en thisque 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 thisenlace en JavaScript.

Pero relevante para nuestra discusión aquí, el valor de thisdentro 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 thisdentro del display()método en este caso es la ventana, o el objeto global, en modo no estricto. En modo estricto, el thisvalor 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 thisvalor 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 thisinside 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 thisinside display()vuelve al enlace predeterminado . Apunta al objeto global o undefinedsi 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 setTimeoutdefinició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.displaya 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 thisvalor 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 thisvalor 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 thisapunta hacia objadentro display().

Incluso si pasamos obj.displaycomo una devolución de llamada, el thisvalor 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 thisque 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 thisvalor fue undefinedcomo 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 thisvalor 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 thisvalor 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 lexicalthisbinding which automatically binds them to the scope they are defined in.