Объясните поведение useMemo. Как избежать ненужных пересчётов?
Хук useMemo в React используется для мемоизации значения, возвращаемого функцией, чтобы избежать повторных дорогостоящих вычислений при каждом рендере компонента. Это особенно полезно, когда результат зависит от сложной логики или обрабатывает большие объёмы данных, а зависимости при этом меняются редко.
Синтаксис
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
-
Первый аргумент — функция, результат которой необходимо запомнить.
-
Второй аргумент — массив зависимостей.
-
Если зависимости не изменились между рендерами, React вернёт закэшированное значение.
Принцип работы
React вызывает функцию () => computeExpensiveValue(a, b) только если хотя бы одна из зависимостей (a, b) изменилась с прошлого рендера. Иначе — берётся сохранённый результат из предыдущего вызова.
Это особенно важно, если функция computeExpensiveValue ресурсоёмкая, например:
-
фильтрация, сортировка, агрегация больших массивов
-
генерация структурированных данных
-
форматирование сложных объектов
Пример: без useMemo
function ExpensiveComponent({ items }) {
const filteredItems = items.filter(item => item.active);
return <List data={filteredItems} />;
}
Здесь filter() будет запускаться на каждом рендере, даже если items не изменились (например, ререндер произошёл из-за изменения состояния в другом месте).
Пример: с useMemo
function ExpensiveComponent({ items }) {
const filteredItems = useMemo(() => {
return items.filter(item => item.active);
}, \[items\]);
return <List data={filteredItems} />;
}
Теперь filter() вызовется только тогда, когда изменится items. Это предотвращает ненужную работу при каждом рендере.
В чём разница между useMemo и useCallback
useCallback | useMemo |
---|---|
Мемоизирует функцию | Мемоизирует возвращаемое значение |
--- | --- |
Возвращает саму функцию | Возвращает результат выполнения |
--- | --- |
Используется для событий/props | Используется для вычислений |
--- | --- |
Когда использовать useMemo
-
Дорогие вычисления
Например, парсинг, фильтрация, сортировка, преобразование данных, работа с графами. -
Стабильность пропсов в дочерних компонентах
Чтобы не передавать каждый раз новый объект или массив, который вызовет лишний ререндер у React.memo. -
Работа с массивами или объектами, передаваемыми по ссылке
React сравнивает пропсы по ссылке (===). Если передавать новый объект/массив при каждом рендере — React.memo или useEffect будут реагировать, даже если содержимое не изменилось.
Пример: мемоизация объекта
const options = useMemo(() => ({
sort: 'asc',
limit: 10,
}), \[\]);
Без useMemo при каждом рендере создаётся новый объект, и useEffect(() => ..., [options]) будет срабатывать каждый раз. С useMemo объект остаётся стабильным, и эффект не перезапускается.
Потенциальные ошибки
- Мемоизация дешёвых значений
Не стоит использовать useMemo ради самого факта использования. Если логика простая и не требует большого количества ресурсов — лучше обойтись без мемоизации. Вызов useMemo сам по себе потребляет немного ресурсов.
Пропущенные зависимости
Если указать не все зависимости, значение может быть рассчитано с устаревшими данными.
Неверно:
```python
const value = useMemo(() => compute(data), []); // data не указано
Верно:
```python
const value = useMemo(() => compute(data), \[data\]);
-
Для надёжности можно использовать ESLint-плагин react-hooks, который предупредит о пропущенных зависимостях.
-
Мемоизация с нестабильными ссылками
Если одна из зависимостей сама изменяется при каждом рендере (например, () => {}), то useMemo будет бесполезен, так как значение будет пересчитываться постоянно. Такие зависимости нужно тоже мемоизировать (например, через useCallback).
useMemo и React.memo
Если React.memo используется на компоненте, который получает в props массив или объект, то без useMemo эти пропсы всегда будут казаться "новыми":
const MyComponent = React.memo(({ data }) => {
console.log("Rendered");
return <div>{data.length}</div>;
});
function App() {
const data = \[1, 2, 3\]; // создаётся заново каждый раз
return <MyComponent data={data} />;
}
Даже если data содержит те же значения, при каждом рендере создаётся новая ссылка. Решение:
const data = useMemo(() => \[1, 2, 3\], \[\]);
Теперь ссылка стабильна, React.memo не вызовет лишний ререндер.
Как избежать ненужных пересчётов
-
Следить за зависимостями. Указывай только те переменные, которые действительно влияют на результат.
-
Избегай мемоизации тривиальных вычислений. Нет смысла мемоизировать x + 1, но есть смысл мемоизировать сортировку 10 000 записей.
-
Не мемоизируй функции в useMemo. Если цель — сохранить функцию, используй useCallback.
-
**Используй useMemo для props сложных компонентов, зависящих от больших данных.
** - Не оборачивай всё подряд. Мемоизация имеет смысл только там, где она реально предотвращает лишнюю работу и даёт выигрыш по производительности.
Вложенные useMemo
Допустимо использовать useMemo внутри другого useMemo, если одна мемоизация зависит от результата другой:
const filtered = useMemo(() => {
return list.filter(item => item.active);
}, \[list\]);
const sorted = useMemo(() => {
return \[...filtered\].sort(compareFn);
}, \[filtered\]);
Такой подход обеспечивает чёткий контроль над пересчётами: filtered не изменится без list, а sorted не изменится без filtered.
Хук useMemo даёт мощный инструмент для оптимизации производительности компонентов, позволяя избежать лишних вычислений и стабилизировать ссылки на данные, особенно важные при работе с React.memo, сложными операциями и динамически вычисляемыми значениями.