Ето защо трябва да обвържем манипулаторите на събития в Class Components в React

Докато работите по React, трябва да сте попаднали на контролирани компоненти и манипулатори на събития. Трябва да обвържем тези методи с екземпляра на компонента, използвайки .bind()в конструктора на нашия персонализиран компонент.

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") );

В тази статия ще разберем защо трябва да направим това.

Бих препоръчал да прочетете .bind()тук, ако все още не знаете какво прави.

Обвинявайте JavaScript, не реагирайте

Е, обвиняването звучи малко грубо. Това не е нещо, което трябва да направим поради начина на работа на React или заради JSX. Това се дължи на начина, по който thisсвързването работи в JavaScript.

Нека видим какво ще се случи, ако не обвържем метода на манипулатора на събития с неговия екземпляр на компонент:

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") );

Ако стартирате този код, кликнете върху бутона „Щракнете върху мен“ и проверете вашата конзола. Ще видите undefinedотпечатано на конзолата като стойност от thisот метода за обработка на събития. В handleClick()метода изглежда са загубили своя контекст (компонент например) или thisстойност.

Как работи това обвързване в JavaScript

Както споменах, това се случва поради начина, по който thisсвързването работи в JavaScript. Няма да навлизам в много подробности в тази публикация, но тук има чудесен ресурс, за да разберете как thisсвързването работи в JavaScript.

Но от значение за нашата дискусия тук, стойността на thisвътрешната функция зависи от начина на извикване на тази функция.

Обвързване по подразбиране

function display(){ console.log(this); // 'this' will point to the global object } display(); 

Това е обикновен функционен разговор. Стойността на thisвътре в display()метода в този случай е прозорецът - или глобалният - обект в не-строг режим. В строг режим thisстойността е undefined.

Неявно обвързване

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

Когато извикаме функция по този начин - предшествана от контекстен обект - thisстойността вътре display()е зададена на obj.

Но когато присвоим тази препратка към друга променлива и извикаме функцията, използвайки тази нова препратка към функцията, получаваме различна стойност thisотвътре display().

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

В горния пример, когато се обаждаме outerDisplay(), не посочваме контекстен обект. Това е обикновено извикване на функция без обект собственик. В този случай стойността thisотвътре display()се връща към свързването по подразбиране . Той сочи към глобалния обект или undefinedако функцията, която се извиква, използва строг режим.

Това е особено приложимо при предаване на такива функции като обратно извикване на друга персонализирана функция, функция на библиотека на трета страна или вградена JavaScript функция като setTimeout.

Помислете за setTimeoutфиктивната дефиниция, както е показано по-долу, и след това я извикайте.

// A dummy implementation of setTimeout function setTimeout(callback, delay){ //wait for 'delay' milliseconds callback(); } setTimeout( obj.display, 1000 );

Можем да разберем, че когато се обаждаме setTimeout, JavaScript вътрешно присвоява obj.displayсвоя аргумент callback.

callback = obj.display;

Тази операция за присвояване, както видяхме по-рано, кара display()функцията да загуби своя контекст. Когато този обратен извикване в крайна сметка се извика вътре setTimeout, thisстойността вътре display()се връща към обвързването по подразбиране .

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

Изрично твърдо подвързване

За да избегнете това, което можем изрично трудно се свързват на thisстойност към функция, с помощта на bind()метода.

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

Сега, когато се обаждаме outerDisplay(), стойността на thisточките objотвътре display().

Дори и да преминем obj.displayкато обратно повикване, thisстойността вътре display()правилно ще сочи obj.

Пресъздаване на сценария, използвайки само JavaScript

В началото на тази статия видяхме това в нашия React компонент, наречен Foo. Ако не сме обвързали манипулатора на събития с this, стойността му вътре в манипулатора на събития е зададена като undefined.

Както споменах и обясних, това се дължи на начина, по който thisсвързването работи в JavaScript и не е свързано с това как работи React. Така че нека да премахнем специфичния за React код и да изградим подобен чист пример на JavaScript, за да симулираме това поведение.

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

Ние не симулираме действителни събития и манипулатори, а вместо това използваме синонимния код. Както забелязахме в примера на React Component, thisстойността беше, undefinedтъй като контекстът беше изгубен след предаване на манипулатора като обратно извикване - синоним на операция за присвояване. Това наблюдаваме и тук, в този фрагмент на JavaScript, който не е React.

„Чакай малко! Не трябва ли thisстойността да сочи към глобалния обект, тъй като изпълняваме това в не-строг режим съгласно правилата на обвързването по подразбиране? " може да попитате.

Не. Ето защо:

Телата на декларациите на класове и класовите изрази се изпълняват в строг режим, т.е. методите на конструктора, статиката и прототипа. Функциите getter и setter се изпълняват в строг режим.

Можете да прочетете цялата статия тук.

Така че, за да предотвратим грешката, трябва да свържем thisстойността по следния начин:

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

Не е нужно да правим това в конструктора и можем да направим това и някъде другаде. Помислете за това:

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.