Что такое React.memo и как он работает?
Прежде чем погрузиться в детали, представьте, что ваш React-компонент — это шеф-повар. Каждый раз, когда поступает заказ (рендеринг), он готовит одно и то же блюдо заново, даже если вчера его уже готовил. Это неэффективно. React.memo — это как холодильник: шеф может посмотреть в него и сказать: "Эй, это блюдо уже готово, я просто достану его из холодильника!"
React.memo — это HOC (Higher-Order Component), который оборачивает функциональный компонент и предотвращает его повторный рендеринг, если его пропсы не изменились.
Как это работает?
React.memo сравнивает текущие пропсы с предыдущими. Если пропсы не изменились, React пропускает повторный рендер компонента.
import React from 'react';
// Наш компонент
type Props = { name: string };
const Greeting: React.FC<Props> = ({ name }) => {
console.log('Рендер компонента Greeting');
return <h1>Привет, {name}!</h1>;
};
// Оборачиваем компонент в React.memo
const MemoizedGreeting = React.memo(Greeting);
export default MemoizedGreeting;
Используя Greeting в вашем приложении:
import React, { useState } from 'react';
import MemoizedGreeting from './MemoizedGreeting';
const App: React.FC = () => {
const [count, setCount] = useState(0);
return (
<div>
<MemoizedGreeting name="Иван" />
<button onClick={() => setCount(count + 1)}>Счёт: {count}</button>
</div>
);
};
Вывод в консоли:
Рендер компонента Greeting
Каждый раз, когда вы нажимаете кнопку, компонент MemoizedGreeting не рендерится снова, так как его пропсы name: "Иван" не изменились.
Примеры использования React.memo
Пример 1. Лишние рендеры в списке
Представим, что вы отображаете список предметов в компоненте, и каждый элемент рендерится независимо. Здесь React.memo поможет не перерисовывать элементы, которые не изменились.
import React from 'react';
type ItemProps = { item: string };
const ListItem: React.FC<ItemProps> = React.memo(({ item }) => {
console.log(`Рендер: ${item}`);
return <li>{item}</li>;
});
type ListProps = { items: string[] };
const ItemList: React.FC<ListProps> = ({ items }) => {
return (
<ul>
{items.map((item, index) => (
<ListItem key={index} item={item} />
))}
</ul>
);
};
export default ItemList;
Пример 2. Изменение ближайшего родителя
Если родительский компонент изменяется, это может триггерить лишние рендеры у дочерних компонентов. С React.memo мы предотвращаем такую цепочку рендеров.
import React, { useState } from 'react';
import ItemList from './ItemList';
const App: React.FC = () => {
const [count, setCount] = useState(0);
const items = ['Яблоко', 'Банан', 'Груша'];
return (
<div>
<button onClick={() => setCount(count + 1)}>Счёт: {count}</button
<ItemList items={items} />
</div>
);
};
На каждый клик кнопки, ItemList рендерится, но сами ListItem не перерисовываются, так как их пропсы не меняются.
Ограничения и лучшие практики
Хорошее правило: оптимизируйте только то, что имеет значение. Если компонент рендерится очень быстро (например, не содержит сложной логики), использование React.memo может быть избыточным и даже замедлит приложение из-за дополнительной проверки пропсов.
Ограничения React.memo
- Сравнение только пропсов:
React.memoне может предотвратить рендер, если компонент зависит от состояния или контекста. - Поверхностное сравнение: по умолчанию, сравнение пропсов выполняется "поверхностно". Если пропс — это объект или массив, React сравнит только ссылки на них, а не их содержимое.
import React from 'react';
// Пример с объектом
type UserProps = { user: { name: string } };
const UserGreeting: React.FC<UserProps> = React.memo(({ user }) => {
console.log('Рендер UserGreeting');
return <h1>Привет, {user.name}!</h1>;
});
const App: React.FC = () => {
const user = { name: 'Иван' };
return <UserGreeting user={user} />;
};
Даже если user не изменится, React снова отрендерит компонент, так как сравниваются ссылки на объект, а не его содержимое.
Решение: кастомный areEqual
React.memo принимает второй аргумент — функцию areEqual, чтобы контролировать процесс сравнения пропсов.
const UserGreeting = React.memo(
({ user }: UserProps) => {
console.log('Рендер UserGreeting');
return <h1>Привет, {user.name}!</h1>;
},
(prevProps, nextProps) => {
return prevProps.user.name === nextProps.user.name;
}
);
Теперь рендер компонента будет предотвращён, если имя пользователя не изменилось.
Лучшие практики использования React.memo
Используйте
React.memo, если компонент:- Получает большие данные в пропсах и нечасто изменяется.
- Выполняет тяжёлые вычисления или сложную логику.
- Используется часто, но изменения в пропсах происходят редко.
Избегайте мемоизации компонентов с контекстом или локальным состоянием —
React.memoникак не влияет на изменения вuseContextилиuseState.Не применяйте
React.memoк каждому компоненту подряд. Оптимизация — это весело, но только если она действительно нужна. Помните: "Меньше — значит больше".
Реальное применение: мемоизация списка задач
Теперь добавим React.memo в наше приложение списка задач.
Компонент задачи
type TaskProps = { task: string };
const TaskItem: React.FC<TaskProps> = React.memo(({ task }) => {
console.log(`Рендер задачи: ${task}`);
return <li>{task}</li>;
});
Список задач
type TaskListProps = { tasks: string[] };
const TaskList: React.FC<TaskListProps> = ({ tasks }) => {
return (
<ul>
{tasks.map((task, index) => (
<TaskItem key={index} task={task} />
))}
</ul>
);
};
Приложение
const App: React.FC = () => {
const [tasks, setTasks] = React.useState(['Купить молоко', 'Выгулять собаку']);
const [count, setCount] = React.useState(0);
const addTask = () => {
setTasks([...tasks, `Новая задача №${tasks.length + 1}`]);
};
return (
<div>
<h1>Список задач</h1>
<TaskList tasks={tasks} />
<button onClick={addTask}>Добавить задачу</button>
<button onClick={() => setCount(count + 1)}>Счёт: {count}</button>
</div>
);
};
Теперь TaskItem рендерится только тогда, когда к списку добавляется новая задача.
В будущем мы еще раз коснемся это темы, так что если вы поняли только половину - это уже отлично :)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ