Подробное описание Pure Component и React.memo () и зачем они нам нужны

React отображает компоненты гораздо больше, чем вы думаете. Давай узнаем почему.

Распространенная ошибка - полагать, что компонент React достаточно умен, чтобы понять, нужно ли ему снова рендерить себя или нет, основываясь на своих собственных props и state. Но это не совсем так.

Чтобы лучше понять, почему, мы должны начать с изучения ограничений компонента, не являющегося чистым React, с двух примеров. После этого будет легче понять преимущества PureComponent и React.memo(), посмотрев на два других примера.

Краткое описание того, что нас ждет:

  • Пример 1. Два компонента класса
  • Примирение
  • Пример 2 - Два функциональных компонента с хуками
  • Пример 3 - один компонент чистого класса + один компонент класса
  • Пример 4 - Один чисто функциональный компонент + один функциональный компонент
  • Еще кое-что
  • Выводы

Пример 1. Два компонента класса

import React, { Fragment } from "react";
class WelcomeMessage extends React.Component {
render() {
console.log("render WelcomeMessage");
return <label>Hello {this.props.user}</label>;
}
}
class Page extends React.Component {
state = { user: "" };
render() {
console.log("render Page");
return (
<Fragment>
<WelcomeMessage user={this.state.user} />
<button onClick={() => this.setState({ user: "Jack" })}>
Set User
</button>
</Fragment>
);
}
}
view raw twoClassComp.js hosted with ❤ by GitHub

Этот код довольно прост: когда Page загружен, значение user в объекте state является пустой строкой. Только после первого щелчка по кнопке устанавливается значение «Джек».

Итак, что вы ожидаете, что будет напечатано в консоли после первого нажатия кнопки?
Вероятно, вы правильно догадались: поскольку объект state обновляется, функция render будет вызвана снова, таким образом, оба console.log сообщения будут распечатаны.

Но как вы думаете, что произойдет, если вы нажмете кнопку второй, третий или более раз?
Ответ на этот вопрос менее тривиален.

Вы можете подумать, что ничего не будет отображаться снова, поскольку похоже, что объект state не изменился. Следовательно, в консоли ничего не должно выводиться.
В конце концов, документация напоминает нам, что React повторно визуализирует компонент, только если его props или state изменились.
Но этого не происходит. В нашем примере каждый компонент будет повторно отображаться каждый раз, когда вы нажимаете кнопку, и оба сообщения будут печататься в консоли снова и снова.

Помните: в React и props, и state являются неизменяемыми объектами.
Это означает, во-первых, что объект state в Page фактически изменяется. Скорее, он воссоздается setState казнью.
Во-вторых, из-за повторного рендеринга Page WelcomeMessage будет вызываться снова с новым экземпляром своего props объекта, который представляет собой совершенно новый объект.

Подсказка - если вы не знакомы с этой концепцией, попробуйте вставить {} === {} в консоль и проверьте результат. Также вы можете ознакомиться с этой статьей.

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

Теперь вы видите ограничения компонента React?

Прежде чем мы перейдем к следующему примеру, важно указать, что именно React делает при каждом рендеринге.

Примирение

В React для рендеринга означает, что вызывается метод render(), но сам метод не обновляет DOM напрямую. Вместо этого React запускает процесс, называемый согласованием, в котором он может понять, какие части DOM следует обновить. Это делается путем сравнения предыдущей модели DOM со следующей.

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

Чтобы избежать ненужных визуализаций и, таким образом, согласования, мы можем использовать PureComponent (с компонентами класса) или React.memo() (с компонентами функций).

Скоро мы увидим их в действии, но сначала давайте посмотрим на следующее.

Пример 2 - Два функциональных компонента с крючками

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

import React, { useState, Fragment } from "react";
const WelcomeMessage = ({ user }) => {
console.log("render WelcomeMessage");
return <label>Hello {user}</label>;
};
const Page = () => {
const [user, setUser] = useState();
console.log("render Page");
return (
<Fragment>
<WelcomeMessage user={user} />
<button onClick={() => setUser("Jack")}>
Set user
</button>
</Fragment>
);
};
view raw twoFuncComp.js hosted with ❤ by GitHub

Давайте сразу перейдем к поведению приложения при нажатии кнопки второй или более раз.
Можно ожидать, что результат будет таким же, как и в компоненте класса в предыдущем примере. Но это не так. Почему?

Здесь состояние компонента, равное user, больше не является объектом, а представляет собой простую строку.

Помните, как React раньше сравнивал два объекта?
В данном случае сравниваются две строки, которые имеют одинаковое значение Джек. Проверка равенства, используемая React, затем вернет true, и поэтому рендеринга не произойдет. Бум!

И, поскольку Page не отображается, WelcomeMessage не вызывается и не отображается снова.

Здесь мы можем обнаружить одно из преимуществ использования хуков 😎

Обратите внимание: при втором щелчке вы фактически увидите журнал рендеринга страницы, но после третьего щелчка такое поведение не повторится. Как указано в официальной документации, React все еще может потребоваться отрендерить компонент (в котором используется ловушка) в последний раз.

А что, если тип user должен быть объектом, а не строкой?

Что ж, в этом случае поведение, к сожалению, будет точно таким же, как и в компоненте класса.

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

Пример 3 - Один компонент чистого класса + один компонент класса

Следующий код точно такой же, как и в первом примере, за исключением компонента Page, который расширяет React.PureComponent вместо React.Component.

import React, { Fragment } from "react";
class WelcomeMessage extends React.Component {
render() {
console.log("render WelcomeMessage");
return <label>Hello {this.props.user}</label>;
}
}
class Page extends React.PureComponent {
state = {};
render() {
console.log("render Page");
return (
<Fragment>
<WelcomeMessage user={this.state.user} />
<button onClick={() => this.setState({ user: "Jack" })}>
Set User
</button>
</Fragment>
);
}
}

Опять же, как вы ожидаете, что приложение будет вести себя при нажатии кнопки второй или более раз?

Поведение, наконец, является тем, что мы ожидали увидеть с самого начала: второй щелчок по кнопке не приводит к появлению журнала и, следовательно, рендеринга не происходит.

Итак, как именно работает PureComponent?

В PureComponent каждый раз при изменении свойств или состояния React выполняет поверхностное сравнение между объектами props и nextProps, а также между объектами state и nextState.
Мелкое сравнение - это сравнение между всеми ключами в первый уровень в объектах.

В нашем сценарии после второго нажатия кнопки чистый компонент Page выполнит неглубокое сравнение state, и, поскольку он не изменился, компонент больше не будет отображаться. Следовательно, компонент WelcomeMessage тоже не будет.

Последний пример аналогичен, но я хотел бы остановиться на нескольких интересных моментах.

Пример 4 - Один чистый функциональный компонент + один функциональный компонент

Следующий код похож на Пример 2, за исключением того, что:

  • Компонент функции WelcomeMessage теперь обернут вокруг React.memo()
  • Значение user - это не строка, а объект
import React, { useState, Fragment } from "react";
const WelcomeMessage = React.memo(({ name }) => {
console.log("render WelcomeMessage");
return <label>Hello {name}</label>;
});
const Page = () => {
const [user, setUser] = useState({});
console.log("render Page");
return (
<Fragment>
<WelcomeMessage name={user.name} />
<button onClick={() => setUser({ name: "Jack", age: 42 })}>
Set user
</button>
</Fragment>
);
};

Перво-наперво: React.memo() выполняет то же самое, что и PureComponent, но не проверяет наличие изменений в state, а только в props.

Важно отметить, что чистый компонент в третьем примере был Page, а теперь это WelcomeMessage.

Итак, в последний раз, что вы ожидаете увидеть после второго щелчка по кнопке?

Ответ - только сообщение журнала «отобразить страницу».

Думаю, вы уже знаете причину, но, конечно же:

  • user теперь объект. Это вызовет повторную визуализацию Page при каждом вызове setUser.
  • WelcomeMessage вместо этого использует React.memo(). Каждый раз, когда компонент визуализируется родителем, будет выполняться поверхностное сравнение. Все props, то есть только строка name, будут сравниваться, чтобы решить, нужно ли визуализировать компонент или нет.

Еще кое-что

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

В этих случаях вы можете сделать следующее:

Для PureComponent существует метод API под названием shouldComponentUpdate. Вы можете использовать его для написания собственной логики сравнения. Ваша логика должна тогда вернуть true, если компонент необходимо обновить, false, если этого не произошло.

shouldComponentUpdate(nextProps, nextState){
// your logic here
}

Для функционального компонента React.memo(Component, areEqual) принимает в качестве второго аргумента функцию сравнения areEqual. Он должен возвращать true, если компонент не нужно обновлять, false, если это необходимо.

function areEqual(){
// your logic here
}
React.memo(MyWelcomeMessage, areEqual)

Выводы

Ответственно подходите к использованию чистых компонентов. Поверхностное сравнение не дается бесплатно. Это дорогостоящее вычисление, и его лучше избегать, если в нем нет необходимости.

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

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

Спасибо за чтение и удачного кодирования!

Спасибо Алессио Ван Кеулену за вычитку статьи.