Разбираем, чем useMemo и useCallback отличаются, когда они бесполезны, и как избежать частых ошибок. Практические советы для React-разработчиков.
Многие React-разработчики запоминают синтаксис useMemo и useCallback, но не понимают, что именно они делают. Это приводит к багам и лишним ререндерам. Сегодня разберем, чем эти хуки отличаются, когда они бесполезны, и как избежать типичных ошибок. Готовы? Поехали.
Многие думают, что это взаимозаменяемые хуки для ускорения. На самом деле:
По сути, useCallback(fn, deps) — это синтаксический сахар для useMemo(() => fn, deps). Разница лишь в том, что useCallback возвращает саму функцию, а useMemo — результат её вызова.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);В первом случае кэшируется результат вычислений, во втором — сама функция.
Оборачивание в useMemo тоже имеет стоимость: на каждом рендере выполняется сравнение зависимостей. Если вычисления внутри дешёвые, вы платите больше, чем экономите. Используйте эти хуки только когда вычисления действительно дорогие, или когда нужно сохранить ссылку (см. пункт 3), а не как рефлекс на каждое производное значение.
const total = useMemo(() => a + b, [a, b]); // избыточно: сложение дешёвое
const filteredList = useMemo(() => list.filter(isValid), [list]); // оправдано, если list большойЕсли передать обычную инлайн-функцию в пропсы мемоизированного дочернего компонента, он всё равно будет ререндериться, потому что функция создаётся заново при каждом рендере. useCallback сохраняет ссылку стабильной, и тогда React.memo может пропустить ререндер.
const Child = React.memo(({ onClick }) => );
function Parent() {
const handleClick = useCallback(() => console.log('click'), []);
return ;
}Без useCallback Child будет ререндериться каждый раз, так как ссылка на функцию меняется.
Здесь та же ловушка с устаревшими замыканиями. Если закэшировать функцию с useCallback(fn, []), а fn читает пропс или состояние извне, вы получите функцию, замороженную со значением на момент первого рендера. Решение — включить эту зависимость в массив, а не подавлять линтер.
function Component({ userId }) {
const fetchData = useCallback(() => {
fetch(`/api/user/${userId}`); // userId берётся из замыкания
}, []); // userId не указан — будет использовано первое значение
// Правильно: [userId]
}Выражение useMemo(() => doSomething(config), [config]) выглядит безопасным, но если config — это { foo: bar }, созданный инлайн на каждом рендере, зависимость меняется каждый раз, и мемоизация не срабатывает. Нужно вынести объект за пределы рендера или мемоизировать его отдельно.
const config = { foo: bar }; // создаётся каждый рендер
const result = useMemo(() => doSomething(config), [config]); // не работает
// Решение:
const config = useMemo(() => ({ foo: bar }), []);
const result = useMemo(() => doSomething(config), [config]);Теперь вы знаете, что useMemo и useCallback — не панацея. Прежде чем использовать их, спросите себя: «Действительно ли вычисление дорогое? Нужна ли стабильная ссылка?» Если нет — не используйте. Если да — не забывайте про зависимости. Попробуйте применить эти знания в своём проекте: найдите место, где useMemo или useCallback используются без необходимости, и удалите их. Замерьте производительность до и после. Увидите разницу — значит, было не зря.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →