React.memo: мемоизация компонентов
Чем сложнее становится приложение, тем важнее следить за его производительностью. Даже в небольшом приложении по учёту расходов может возникнуть ситуация, когда при каждом вводе данных перерисовываются все компоненты — даже те, которые к этим данным не имеют никакого отношения.
Сегодня мы рассмотрим два инструмента оптимизации:
React.memo— для предотвращения лишних рендеров компонентов;useCallback— для мемоизации функций, чтобы не передавать их заново при каждом рендере.
Они особенно полезны, если в вашем приложении:
- Они особенно полезны, если в вашем приложении:
- есть часто обновляемое состояние (например, форма ввода);
- много взаимодействий между родительскими и дочерними компонентами.
React.memo: оптимизация компонентов
React.memo — это обёртка, которая запоминает результат рендера компонента и использует его повторно, если пропсы не изменились.
Это особенно полезно в списках: если у нас 100 транзакций, и изменяется только одна — нет смысла перерисовывать остальные 99.
Пример: компонент транзакции
Файл src/components/TransactionItem.tsx
import React from "react";
interface TransactionItemProps {
category: string;
amount: number;
date: string;
type: "income" | "expense";
}
const TransactionItem: React.FC<TransactionItemProps> = ({ category, amount, date, type }) => {
console.log(`Рендерится транзакция: ${category}`);
return (
<li>
{date.slice(0, 10)} — {category}: {type === "income" ? "+" : "-"}${amount}
</li>
);
};
// Оборачиваем в React.memo
export default React.memo(TransactionItem);
Теперь этот компонент будет перерендериваться только при изменении его пропсов.
useCallback: мемоизация функций
Функции в JavaScript — это объекты. Поэтому при каждом рендере создаётся новая функция, даже если она выглядит одинаково. Это мешает React.memo, потому что "старый" и "новый" пропс выглядят разными.
Пример без useCallback
const Parent = () => {
const handleClick = () => {
console.log("Нажато");
};
return <Child onClick={handleClick} />;
};
Каждый раз при рендере Parent, handleClick создаётся заново — а значит, Child думает, что пропсы изменились и тоже перерендеривается.
Решение: useCallback
Пример с useCallback
import React, { useCallback } from "react";
const Parent: React.FC = () => {
const handleClick = useCallback(() => {
console.log("Нажато");
}, []); // Функция не изменится, пока зависимости не изменятся
return <Child onClick={handleClick} />;
};
const Child: React.FC<{ onClick: () => void }> = React.memo(({ onClick }) => {
console.log("Рендерится Child");
return <button onClick={onClick}>Нажми меня</button>;
});
Теперь handleClick не пересоздаётся при каждом рендере, а Child не перерендеривается без причины.
Применение в проекте: список транзакций
Представим, что у нас есть список транзакций и кнопка "Удалить". Нам важно, чтобы при добавлении одной транзакции остальные не перерисовывались.
Файл src/components/TransactionItem.tsx
import React from "react";
interface Transaction {
id: number;
category: string;
amount: number;
date: string;
type: "income" | "expense";
onDelete: (id: number) => void;
}
const TransactionItem: React.FC<Transaction> = ({ id, category, amount, date, type, onDelete }) => {
console.log(`Рендер: ${category}`);
return (
<li>
{date.slice(0, 10)} — {category}: {type === "income" ? "+" : "-"}${amount}
<button onClick={() => onDelete(id)}>Удалить</button>
</li>
);
};
export default React.memo(TransactionItem);
Файл src/components/TransactionList.tsx
import React, { useCallback } from "react";
import TransactionItem from "./TransactionItem";
interface Transaction {
id: number;
category: string;
amount: number;
date: string;
type: "income" | "expense";
}
interface Props {
transactions: Transaction[];
onDelete: (id: number) => void;
}
const TransactionList: React.FC<Props> = ({ transactions, onDelete }) => {
const handleDelete = useCallback(
(id: number) => {
onDelete(id);
},
[onDelete]
);
return (
<ul>
{transactions.map((tx) => (
<TransactionItem key={tx.id} {...tx} onDelete={handleDelete} />
))}
</ul>
);
};
export default TransactionList;
Что здесь оптимизировано:
TransactionItemобёрнут вReact.memo— не будет ререндериться без изменения пропсов.handleDeleteмемоизирован с помощьюuseCallback— не пересоздаётся при каждом рендере списка.
Общие ошибки и рекомендации
- Мемоизируйте только "тяжёлые" компоненты. Не нужно оборачивать
React.memoвсё подряд. Часто проще перерендерить лёгкий компонент, чем проверять, изменились ли пропсы. - Следите за зависимостями в
useCallback. Если забыть добавить зависимость, функция может использовать устаревшие значения. - Не усложняйте код ради оптимизации. Если производительность нормальная, лучше оставить код простым.
- Сравнение объектов и массивов всегда возвращает
false. Даже если массив[]выглядит одинаково, это два разных объекта в памяти.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ