Зарегистрируйтесь, чтобы продолжить обучение

Хуки useCallback и useMemo JS: React Hooks

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

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

Самый простой способ отследить перерисовку компонента — это расставить точки остановки выполнения кода в дебагере, либо добавить вывод лога.

Ниже простое приложение из двух компонентов:

import { useState, useEffect } from 'react';

const Greeting = () => {
  useEffect(() => {
    console.log(`Компонент Greeting отрисован в ${new Date().toLocaleTimeString()}`);
  });

  return <h3>Hello, world!</h3>;
};

const App = () => {
  const [name, setName] = useState('');
  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <Greeting />
    </>
  );
};

В компонент Greeting мы добавили вывод в console.log() с меткой о времени для наглядности. Так мы сможем отследить время отрисовки компонента. Компонент App использует в представлении Greeting и содержит поле ввода, в котором меняется состояние компонента.

Если ввести значение в input, то в консоли браузера мы увидим вывод сообщения об отрисовке компонента Greeting. Компонент будет отрисовываться каждый раз, когда мы вводим значение в поле. Добавляем или удаляем один символ — и компонент перерисовывается. Если мы напечатаем слово hello, компонент перерисуется пять раз.

Расширение Profiler

Есть более удобный способ отслеживать отрисовку компонентов. Для этого нужно установить расширения для браузера React Developer Tools. Расширение добавляет несколько инструментов для работы с React-приложением. Один из них — это Profiler, позволяющий отслеживать отрисовку компонентов.

Нужно открыть вкладку Profiler в devtools, нажать кнопку записи слева и после этого взаимодействовать с приложением — например, ввести в поле любое значение, после этого остановить запись

Так будет выглядеть результат, если ввести в поле строку "hello":

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

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

Обычно при сборке React-приложение оптимизируется и поддержка профайлера из него убирается. Но вы можете использовать флаг --profile в команде сборки, если используете create-react-app, чтобы оставить поддержку инструмента на сервере:

npm run build -- --profile

Инструмент memo

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

Такое поведение можно изменить, в React для этого используется инструмент мемоизации memo. Достаточно в него передать наш компонент:

import { memo } from 'react';

const Memoized = memo(MyComponent);

Вот как будет выглядеть наше приложение:

import { memo, useState, useEffect } from 'react';

const Greeting = memo(() => {
  useEffect(() => {
    console.log(`Компонент Greeting отрисован в ${new Date().toLocaleTimeString()}`);
  });

  return <h3>Hello, world!</h3>;
});

const App = () => {
  const [name, setName] = useState('');
  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <Greeting />
    </>
  );
};

Теперь, если ввести в поле значение, то по профайлеру или выводу логов мы увидим, что компонент Greeting не отрисовывается.

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

import { memo, useState, useEffect } from 'react';

const Greeting = memo(({ name }) => {
  useEffect(() => {
    console.log(`Компонент Greeting отрисован в ${new Date().toLocaleTimeString()}`);
  });

  return <h3>{`Hello, ${name}!`}</h3>;
});

const App = () => {
  const [name, setName] = useState('');
  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <Greeting name={name} />
    </>
  );
};

Мемоизация и useMemo

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

import { memo, useState } from 'react';

const Button = memo(({ onClick }) => {
  console.log(`Компонент Button отрисован в ${new Date().toLocaleTimeString()}`);

  return <button onClick={onClick}>Нажми меня</button>;
});

const Greeting = memo(({ name }) => {
  console.log(`Компонент Greeting отрисован в ${new Date().toLocaleTimeString()}`);

  return <h3>{`Hello, ${name}!`}</h3>;
});

const App = () => {
  const [name, setName] = useState('');
  const buttonHandler = () => setName('world');
  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <Greeting name={name} />
      <Button onClick={buttonHandler} />
    </>
  );
};

В приложение добавлен компонент Button, который принимает функцию для события onClick. Несмотря на то, что компонент мемоизирован, он все равно будет перерисовываться при каждой отрисовке App. Это происходит, потому что в компонент передается функция buttonHandler(). При каждой отрисовке App создается новая функция. React видит изменение ссылки на функцию и вызывает перерисовку компонента Button.

Чтобы избежать ненужных перерисовок компонента Button, нужно использовать хук useMemo. Он принимает функцию, которая возвращает какой-то результат. Хук запоминает этот результат и возвращает его каждый раз, не вызывая повторно переданную функцию.

Первым параметром в хук передается функция, возвращаемое значение которой нужно запомнить, а вторым параметром — массив зависимостей. При изменении любой из указанных зависимостей будет вызываться переданная функция и вычисляться новый результат. С этой точки зрения useMemo похож на useEffect.

Доработаем наше приложение так, чтобы в качестве результата возвращалась функция:

import { memo, useState, useMemo } from 'react';

const Button = memo(({ onClick }) => {
  console.log(`Компонент Button отрисован в ${new Date().toLocaleTimeString()}`);
  return <button onClick={onClick}>Нажми меня</button>;
});

const Greeting = memo(({ name }) => {
  console.log(`Компонент Greeting отрисован в ${new Date().toLocaleTimeString()}`);

  return <h3>{`Hello, ${name}!`}</h3>;
});

const App = () => {
  const [name, setName] = useState('');
  const buttonHandler = useMemo(() => () => setName('world'), []);
  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <Greeting name={name} />
      <Button onClick={buttonHandler} />
    </>
  );
};

В useMemo передается функция () => () => setName('world'). Эта функция возвращает функцию () => setName('world'). В итоге эта возвращенная функция будет мемоизирована, и при отрисовке компонента App ссылка на функцию в переменной buttonHandler будет оставаться той же. Компонент Button перерисовываться не будет.

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

Важно знать, что useMemo() не гарантирует, что не будет нового вычисления. В какой-то момент React может вызвать мемоизированную функцию — обычно это происходит для освобождения новых ресурсов. Это значит, что вы не должны строить логику приложения полагаясь на useMemo(), эта функция предназначена лишь для оптимизации работы компонентов.

useCallback

Хук useCallback() работает похожим образом как useMemo, только уже мемоизирует не результат вызова переданной функции, а саму функцию. Это позволяет немного сократить код и избавиться от лишних объявлений функций:

const buttonHandler = useCallback(() => setName('world'), []);

Как и useMemo, useCallback принимает вторым параметром массив зависимостей.

Выводы

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

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


Дополнительные материалы

  1. React.memo
  2. Использование хука useMemo
  3. Использование хука useCallback

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

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

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
Верстка на HTML5 и CSS3, Программирование на JavaScript в браузере, разработка клиентских приложений используя React
10 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на JavaScript в браузере и на сервере (Node.js), разработка бекендов на Fastify и фронтенда на React
16 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»