Хук оптимизации производительности useCallback в React

В этом уроке мы рассмотрим следующий хук для оптимизации производительности useCallback.

Хук useCallback подобен API useMemo, отличие заключается в том, что первый кэширует значение между моментами перерисовки экрана, а второй - коллбэк. Это позволяет нам не перезапускать ресурсозатратные функции, когда это не требуется и может использоваться при передаче функции в дочерние компоненты.

Давайте разберемся подробнее на примере. Для начала создадим компонент App и заведем в нем стейт num:

App.jsxconst [num, setNum] = useState(0);

Пусть у нас будет кнопка, по клику на которую num увеличивается на 1, и абзац, в котором мы будем выводить значение num:

App.jsxreturn (
  <div>
    <button onClick={() => setNum(num + 1)}>click</button>
    <p>clicks: {num}</p>
  </div>
);

А сейчас, предположим, что у нас в App выводится еще какой-то список с элементами, который мы будем дополнять по нажатию другой кнопки. Для хранения элементов этого списка мы заведем стейт items:

App.jsxconst [items, setItems] = useState([]);

И затем напишем функцию addItem для их добавления:

App.jsxfunction addItem() {
  setItems([...items, 'new item']);
}

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

Items.jsxfunction Items({ items, addItem }) {

  const result = items.map((item, index) => {
    return <p key={index}>{item}</p>;
  });
  
  console.log('Items render');
  
  return (
    <div>
      <h3>Our items</h3>
      {result}
      <button onClick={addItem}>add item</button>
    </div>
  );
}

export default Items;

Разместим Items в конце компонента App и будем передавать ему массив items и функцию для добавления элементов addItem:

App.jsxreturn (
  <>
    <div>
      <button onClick={() => setNum(num + 1)}>click</button>
      <p>clicks: {num}</p>
      <br />
    </div>
    <Items items={items} addItem={addItem} />
  </>
);

А теперь понажимаем на кнопочки и убедимся, что num растет и новые элементы добавляются в список. А открыв консоль, мы увидим, что наш список перерисовывается каждый раз, даже если мы кликаем по кнопке, которая увеличивает num.

Если у нас маленький списочек, то все в порядке, а если предполагается, что он будет объемным и там много чего еще есть? Не беда - скажете вы, ведь на прошлом уроке мы рассмотрели API memo, чтобы как раз избегать ненужных перерисовок компонента.

Так давайте обернем наш компонент Items в memo и все дела. Кстати это можно сделать прямо при экспорте Items:

Items.jsxexport default memo(Items);

Не забудем импортировать memo:

Items.jsximport { memo } from 'react';

А теперь откроем консоль и понажимаем на кнопочки. Все старания впустую! Мы мемоизировали компонент, но при нажатии на кнопку 'click' компонент Items все равно перерисовывается каждый раз.

Дело все в том, что когда родительский компонент перерисовывается, его функции пересоздаются заново - это касается и нашей функции addItem, которую мы передаем в Items.

Именно в этот момент нам поможет хук useCallback. Давайте применим его. Для начала импортируем его в App:

App.jsximport { useCallback } from 'react';

Затем переделаем простое объявление функции addItem в Function Expression, укажем в качестве первого параметра для useCallback нашу функцию в виде колбэка. Вторым параметром в квадратных скобочках укажем зависимости - все реактивные переменные, участвующие в функции, в нашем случае это массив items:

App.jsxconst addItem = useCallback(() => {
  setItems(() => [...items, 'New item']);
}, [items]);

Готово! Таким образом мы закэшировали функцию. Нажимаем снова на кнопочки и видим, что теперь при нажатии на кнопку 'click' наш дочерний компонент не перерисовывается.

Задание 1

Создайте компонент App, поместите в него абзац с текстом. Заведите стейт с начальным значением 'text' и выведите его в абзаце. Пусть по клику на абзац ему в конец текста добавляется восклицательный знак.

Задание 2

Создайте дочерний компонент Products, в котором у вас будет кнопка для добавления нового продукта. Разместите его в App. В родительском компоненте создайте стейт с массивом продуктов и функцию добавления нового продукта. Передайте их в качестве пропсов в дочерний, выведите в нем переданный массив в виде списка ul.

Задание 3

В Products выведите в консоль текст 'products render'. Оберните Products в memo. Покликайте на абзац и кнопку. Убедитесь, что при клике на абзац дочерний компонент все равно перерисовывается.

Задание 4

Закэшируйте функцию для добавления продуктов, обернув ее в хук useCallback. Покликайте на абзац и кнопку. Убедитесь, что при клике на абзац, дочерний компонент больше не перерисовывается.