Это шестая часть моих заметок о Руководстве по ReactJS для начинающих от egghead.io.

Используйте компоненты класса с React

В этом разделе будет показано, как неявные привязки this теряются при обновлении состояния в React и как с этим бороться разными способами. Название видео было бы лучше назвать «Состояние настройки и this».

Неявное связывание и его потеря

Привязка this определяется этими правилами в порядке приоритета:

  1. Если вызывается new, this привязывается к вновь созданному объекту.
  2. Если вызывается call или apply (или bind), this привязывается к указанному объекту.
  3. Если вызывается объект контекста, владеющий вызовом, this привязывается к объекту контекста.
  4. this по умолчанию — это undefined в строгом режиме или глобальный объект в противном случае.

Мы сосредоточимся на пункте 3, который называется неявное связывание.

Это ванильный пример JavaScript с неявной привязкой и потерей неявной привязки.

var name = "GLOBAL OBJECT NAME";
function greet (name) {
  return `Hi ${name}, my name is ${this.name}!`
}
var bob = {
  name: 'Bob',
  greet: greet
}
bob.greet('Jane'); // Hi Jane, my name is Bob!
var greetFn = bob.greet; // this binding is lost!
greetFn('Jane'); // // Hi Bob, my name is GLOBAL OBJECT NAME!

Хотя bob не "владеет" функцией greet, можно сказать, что она принадлежит в момент вызова bob.greet. В этой ситуации мы говорим, что bob является объектом контекста, а this "неявно" связано с bob.

Привязка this к bob теряется, когда bob.greet присваивается простой функции greetFn, потому что greetFn является простой функцией, которая ссылается на greet. Применяется привязка по умолчанию, и GLOBAL OBJECT NAME регистрируется в консоли.

Важно отметить, что хотя greetFn выглядит как ссылка на bob.greet, на самом деле это просто ссылка на функцию greet.

Давайте посмотрим на что-то подобное:

var bob = {
  name: 'Bob',
  greet(name) {
    return `Hi ${name}, my name is ${this.name}`;
  }
}
bob.greet('Jane'); // "Hi Jane, my name is Bob"
var greetFn = bob.greet;
greetFn('Jane'); // "Hi Jane, my name is !"

Поскольку глобальной переменной name нет, ничего не возвращается.

Теперь мы можем понять, как происходит нечто подобное при обновлении состояния в React, и как с этим бороться.

Потеря привязки this в компонентах класса

Мы начнем с создания кнопки, которая увеличивает число при нажатии.

class Counter extends React.Component {
  constructor(...args) {
    super(...args)
    this.state = {count: 0}
  }
  render() {
    return (
      <button 
        onClick={() =>
          this.setState(({count}) => ({
            count: count + 1,
        }))}
      >
        {this.state.count}
      </button>
    )
  }
}
ReactDOM.render(
  <Counter />,
  document.getElementById('root'),
)

Сосредоточьтесь на значении onClick. У нас есть стрелочная функция, которая увеличивает состояние на this.setState.

Хотя этот код полностью функционален, мы могли бы назначить функцию this.setState обработчику событий для большей удобочитаемости.

Отредактированный код будет выглядеть так:

handleClick() {
          this.setState(({count}) => ({
            count: count + 1,
        }))
  }
  render() {
    return (
      <button 
        onClick={this.handleClick}
      >
        {this.state.count}
      </button>
    )
  }

О, о! Это больше не работает. Если мы откроем консоль разработчика, то получим Uncaught TypeError: Cannot read property 'setState' of undefined.

Привязка this в this.setState к объекту, созданному классом Counter, была потеряна, когда мы присвоили его handleClick(). Мы просто присваиваем ссылку на функцию setState handleClick(). handleClick() тоже обычная функция. Поскольку классы в ES6 всегда выполняются в строгом режиме, this по умолчанию равно undefined. Обратите внимание на параллели с приведенным выше примером в JavaScript.

Сохранение привязки this в компонентах класса

Мы рассмотрим несколько способов гарантировать, что привязка this не потеряется.

Один из способов использует «явную привязку» или правило номер 2 из приведенных выше this правил привязки. Это когда мы явно говорим, к чему мы хотим привязать this, с помощью call, apply или bind.

Здесь мы будем использовать встроенную утилиту bind в задании onClick. bind возвращает новую функцию, которая жестко закодирована для вызова исходной функции с указанным вами объектом контекста this. Этот вариант явного связывания, включающий назначение выражений call, apply или bind функциям, называется «жестким связыванием».

Таймер будет работать правильно, если мы отредактируем onClick следующим образом:

onClick={this.handleClick.bind(this)}

Хотя это работает, в некоторых ситуациях это может быть узким местом в производительности.

Чтобы устранить узкое место, мы можем сослаться на прототипный метод handleClick(), добавив this.handleClick в конструктор и назначив ему предварительно привязанный метод handleClick.

Конструктор будет выглядеть так:

constructor(...args) {
    super(...args)
    this.state = {count: 0}
    this.handleClick = this.handleClick.bind(this)
  }

Это то же самое, что и общий шаблон лексического захвата this в коде до ES6, например, в var self = this;.

Это решение может быть громоздким, если нам нужно сделать это для множества методов.

Давайте посмотрим на другое решение с открытыми полями класса. В двух словах, это позволяет нам объявлять свойства класса без конструктора.

Посмотрите на приведенный ниже код до и после реализации полей общедоступного класса.

До:

constructor(...args) {
    super(...args)
    this.state = {count: 0}
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
          this.setState(({count}) => ({
            count: count + 1,
        }))
  }

После:

state = {count: 0}
  handleClick = this.handleClick.bind(this)
  handleClick() {
          this.setState(({count}) => ({
            count: count + 1,
        }))
  }

Мы переместили присваивания this.state и this.handeClick из конструктора в тело класса. this. больше не нужен, так как state и handleClick находятся в теле класса. state и handeClick — это общедоступные поля класса, и мы присваиваем им выражения. Поскольку конструктор стал таким же, как конструктор по умолчанию, мы удалили его.

Увеличивающий счетчик будет работать с этой реализацией.

Мы могли бы реорганизовать это, удалив метод handleClick() из прототипа и сохранив его только в экземпляре.

state = {count: 0}
  handleClick = function() {
          this.setState(({count}) => ({
            count: count + 1,
        }))
  }.bind(this)

Пока это работает, мы должны вызывать метод .bind каждый раз, когда мы обновляем состояние. Мы можем использовать лексический this, который возможен со стрелочными функциями, чтобы обойти это.

handleClick = () => {
          this.setState(({count}) => ({
            count: count + 1,
        }))
  }

Какой способ лучше?

Мы обсудили четыре основных способа:

  1. Жесткая привязка метода с .bind в значении назначения обработчика событий
  2. Лексический захват метода прототипа в конструкторе
  3. Вариант № 2 с использованием полей общедоступного класса
  4. Использование лексического this через стрелочные функции

Проблема с номером 1 заключается в том, что в некоторых ситуациях он может быть узким местом в производительности. Номер 2 может быть громоздким, если нам нужно сделать это для многих методов. Номер 3 в настоящее время является экспериментальной реализацией; это, вероятно, не очень хорошо для производственного кода (хотя это обычно используется во многих проектах).

Номер 4 работает и очень распространен, но не без критики. Автор YDKJS Кайл Симпсон говорит, что использование лексического this укрепляет плохую практику уклонения от четкого понимания this. Он предлагает людям либо придерживаться кода лексического стиля, либо принять this и избегать лексического this.

В любом случае важно понимать как жесткую привязку, так и лексику this, чтобы понимать код других и адаптироваться к любой ситуации при работе в команде.

TL;DR

Будьте осторожны, чтобы не потерять привязку this при обновлении состояния в компонентах класса. Вы можете сохранить привязку либо с помощью метода .bind, захватив лексическое this с помощью присваивания в конструкторе или с помощью полей общедоступного класса, либо с помощью лексического this, включенного в стрелочные функции.

Управляйте DOM с помощью React refs

В этом разделе объясняется, как манипулировать узлом DOM напрямую с помощью свойства React ref. Иногда это необходимо для работы некоторых библиотек JavaScript или когда мы хотим получить значение полей формы.

Мы будем использовать vanilla-tilt.js в качестве примера того, как сделать библиотеку JavaScript функциональной с помощью ref.

Создание статического изображения

Это наш базовый код:

class Tilt extends React.Component {
  render() {
    return (
      <div className="tilt-root">
       <div className="tilt-child">
         <div {...this.props} />
       </div>
      </div>
    )
  }
}
const element = (
  <div className="totally-centered">
    <Tilt>
      <div className="totally-centered">
        vanilla-tilt.js
      </div>
    </Tilt>
  </div>
)
ReactDOM.render(
  element,
  document.getElementById('root'),
)

Класс Tilt отображает div с классом tilt-root, в который вложен div с классом tilt-child, в который вложен div, распространяющий свойства. tilt-root стилизует один div с цветным градиентом, а tilt-child представляет собой меньшую белую рамку. У обоих также есть стили для анимации, которые мы увидим позже.

Затем мы создаем константу element, которая представляет собой компонент Tilt, заключенный в div с классом totally-centered. Внутри Tilt есть еще один div с тем же классом со словами «vanilla-tilt.js». totally-centered — это стили flexbox, которые центрируют контент по горизонтали и вертикали.

Этот код создает это статическое изображение:

Добавление ref

Взято из React docs,

Атрибут ref принимает функцию обратного вызова, и обратный вызов будет выполнен сразу после монтирования или размонтирования компонента.

Когда атрибут ref используется в элементе HTML, обратный вызов ref получает базовый элемент DOM в качестве аргумента.

В нашем коде мы будем использовать обратный вызов ref для хранения ссылки на желаемый узел DOM.

Метод рендеринга теперь возвращает это:

return (
      <div
       ref={node => (this.rootNode = node)}
       className="tilt-root">
       <div className="tilt-child">
         <div {...this.props} />
       </div>
      </div>

Хорошей практикой является не назначать стрелочные функции ссылкам, так как назначения стрелочных функций создаются заново при каждом повторном рендеринге и по другим причинам.

Мы могли бы переписать код так:

 setRef = node => {
   return this.rootNode = node
 }
   render() {
   return (
     <div
       ref={this.setRef}

Мы видим, что обращаемся к нужному узлу:

componentDidMount() {
    console.log(this.rootNode)
  }

Использование узла с библиотекой

Теперь мы можем использовать this.rootNode с библиотекой vanilla-tilt.

Вносим библиотеку vanilla-tilt со скриптом <script src="https://unpkg.com/[email protected]/dist/vanilla-tilt.min.js"></script>.

Затем мы можем использовать библиотеку vanilla-tilt, поместив VanillaTilt global в метод ComponentDidMount, потому что мы можем получить доступ к узлу после монтирования компонента. Мы инициализируем VanillaTilt с помощью init, затем передаем некоторые параметры узлу (this.rootNode). Наш новый код выглядит так:

componentDidMount() {
    VanillaTilt.init(this.rootNode,{
      max: 25,
      speed: 400,
      glare: true,
      'max-glare': 0.5,
    })
  }

Оно работает!

TL;DR

Чтобы управлять DOM,

  1. Передайте ref элементу, который вы визуализируете. Помещение ref в класс ссылается на экземпляр этого класса.
  2. В значении ref передайте узел в качестве аргумента и верните назначение узла значению в экземпляре (например: this.rootNode).
  3. После того, как компонент смонтирован, узел можно использовать для библиотеки или чего-то еще.

Редактировать: Спасибо Эдди Уилсону за совет по объявлению ссылок в теле класса!