Ось чому нам потрібно пов’язати обробники подій у компонентах класу в 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") );

Якщо ви запускаєте цей код, натисніть кнопку “Click Me” і перевірте свою консоль. Ви побачите 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(), ми не вказуємо об’єкт контексту. Це звичайний виклик функції без об’єкта власника. У цьому випадку значення thisinside display()повертається до прив'язки за замовчуванням . Він вказує на глобальний об'єкт або undefinedякщо функція, що викликається, використовує жорсткий режим.

Це особливо застосовно при передачі таких функцій, як зворотні виклики іншій користувацькій функції, сторонній бібліотечній функції або вбудованій функції JavaScript, як setTimeout.

Розгляньте setTimeoutфіктивне визначення, як показано нижче, а потім застосуйте його.

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

Ми можемо зрозуміти, що під час дзвінка setTimeoutJavaScript внутрішньо призначає 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, який не реагує.

"Почекай хвилинку! Чи не повинно thisзначення вказувати на глобальний об'єкт, оскільки ми запускаємо це в несуворому режимі згідно з правилами прив'язки за замовчуванням? " Ви можете запитати.

Ні. Ось чому:

Тіла оголошень класів та вирази класів виконуються в строгому режимі, тобто методах конструктора, статики та прототипу. Функції геттера та сеттера виконуються в суворому режимі.

Повну статтю ви можете прочитати тут.

Отже, щоб запобігти помилці, нам потрібно прив’язати 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.