JavaRush /Курсы /Модуль 3: React /Использование React.memo для предотвращения лишних рендер...

Использование React.memo для предотвращения лишних рендеров

Модуль 3: React
25 уровень , 2 лекция
Открыта

Введение

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

React.memo — это HOC (Higher-Order Component), который позволяет избежать ненужных ререндеров компонентов, если их пропсы не изменились. Он делает "мемоизацию" компонента, запоминая его результат рендера при одних и тех же пропсах.

Пример избыточного рендера

// ParentComponent.tsx
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  console.log('ParentComponent rendered');

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ChildComponent name="React Enthusiast" />
    </div>
  );
};

export default ParentComponent;
// ChildComponent.tsx
import React from 'react';

const ChildComponent = ({ name }: { name: string }) => {
  console.log('ChildComponent rendered');
  return <p>Hello, {name}!</p>;
};

export default ChildComponent;

Сценарий такой: при каждом нажатии на кнопку в родительском компоненте, заново рендерится и ParentComponent, и ChildComponent. Это происходит, несмотря на то, что ChildComponent никак не зависит от состояния count. Такой избыточный рендер становится бесполезным.

Как работает React.memo?

Чтобы исправить описанную ситуацию, мы можем обернуть ChildComponent в React.memo. Давайте попробуем:

// ChildComponent.tsx
import React from 'react';

const ChildComponent = React.memo(({ name }: { name: string }) => {
  console.log('ChildComponent rendered');
  return <p>Hello, {name}!</p>;
});

export default ChildComponent;

Теперь ChildComponent не будет рендериться заново, если только значение пропсов name не изменится.

Что делает React.memo?

  1. Сравнение пропсов: по умолчанию React.memo использует поверхностное сравнение пропсов ===.
  2. Кэширование результата: ели пропсы те же самые, возвращается закэшированный результат рендера, и React пропускает этап повторного рендера.

Сценарии применения React.memo

  1. Чистые компоненты: когда ваш компонент зависит только от пропсов и их значения редко меняются.
  2. Большие списки: например, список из 1000 элементов, где каждому элементу соответствует отдельный компонент. Вы не захотите, чтобы обновление одного элемента приводило к ререндеру всех остальных.
  3. Приёмные компоненты в контексте родителя: иногда родительский компонент обновляется часто, а дочерний — нет.

Типизация компонентов с React.memo

Когда мы используем TypeScript, нам нужно правильно типизировать мемоизированные компоненты. Например:

import React from 'react';

interface ChildProps {
  name: string;
}

const ChildComponent: React.FC<ChildProps> = React.memo(({ name }) => {
  console.log('ChildComponent rendered');
  return <p>Hello, {name}!</p>;
});

export default ChildComponent;

Если вы используете интерфейсы для пропсов, React.memo будет работать корректно, наследуя типизацию.

Проблемы и особенности React.memo

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

  1. Кэширование требует памяти. Если компонент сложный и редко обновляется, кэширование может затруднить сборку мусора.
  2. Часто обновляющиеся пропсы. Если пропсы компонента меняются часто, React.memo бесполезен.
  3. Глубокое сравнение пропсов. По умолчанию используется только поверхностное сравнение. Если вам нужно сравнение содержимого объектов или массивов, придётся подключить кастомную функцию сравнения.

Кастомная функция сравнения

React.memo позволяет передать функцию для сравнения пропсов. Например:

import React from 'react';

interface ChildProps {
  data: { key: string };
}

const ChildComponent: React.FC<ChildProps> = React.memo(
  ({ data }) => {
    console.log('ChildComponent rendered');
    return <p>Key: {data.key}</p>;
  },
  (prevProps, nextProps) => {
    // Сравниваем содержимое объекта data
    return prevProps.data.key === nextProps.data.key;
  }
);

export default ChildComponent;

Теперь рендер произойдёт только в случае изменения ключа.

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

// ListPage.tsx
import React, { useState } from 'react';
import ProductItem from './ProductItem';

const ListPage = () => {
  const [counter, setCounter] = useState(0);
  const products = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Product ${i}` }));

  return (
    <div>
      <h1>Products</h1>
      <p>Counter: {counter}</p>
      <button onClick={() => setCounter(counter + 1)}>Update Counter</button>
      {products.map(product => (
        <ProductItem key={product.id} name={product.name} />
      ))}
    </div>
  );
};

export default ListPage;
// ProductItem.tsx
import React from 'react';

interface ProductProps {
  name: string;
}

const ProductItem: React.FC<ProductProps> = React.memo(({ name }) => {
  console.log(`Rendered: ${name}`);
  return <div>{name}</div>;
});

export default ProductItem;

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

Типичные ошибки и пути их решения

Часто новички допускают несколько ошибок при использовании React.memo. Например:

  1. Неправильное ожидание поведения. Многие ожидают, что React.memo магическим образом ускорит всё приложение. Но если компонент и так обновляется редко, выигрыш будет минимальным.
  2. Применение к банальным компонентам. Не применяйте мемоизацию к очень простым компонентам — в этом нет необходимости.
  3. Объекты и массивы как пропсы. Если пропсы передаются как объекты или массивы, React.memo будет считать их изменившимися, даже если содержимое осталось тем же. Решение: мемоизируйте объекты, например, с помощью useMemo.

Пример правильной мемоизации массива:

const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const data = useMemo(() => [{ id: 1, name: 'Item 1' }], []); // Мемоизируем массив

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ChildComponent data={data} />
    </div>
  );
};
2
Задача
Модуль 3: React, 25 уровень, 2 лекция
Недоступна
Мемозированный список
Мемозированный список
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ