Объясните поведение 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

  1. Дорогие вычисления
    Например, парсинг, фильтрация, сортировка, преобразование данных, работа с графами.

  2. Стабильность пропсов в дочерних компонентах
    Чтобы не передавать каждый раз новый объект или массив, который вызовет лишний ререндер у React.memo.

  3. Работа с массивами или объектами, передаваемыми по ссылке
    React сравнивает пропсы по ссылке (===). Если передавать новый объект/массив при каждом рендере — React.memo или useEffect будут реагировать, даже если содержимое не изменилось.

Пример: мемоизация объекта

const options = useMemo(() => ({
sort: 'asc',
limit: 10,
}), \[\]);

Без useMemo при каждом рендере создаётся новый объект, и useEffect(() => ..., [options]) будет срабатывать каждый раз. С useMemo объект остаётся стабильным, и эффект не перезапускается.

Потенциальные ошибки

  1. Мемоизация дешёвых значений
    Не стоит использовать useMemo ради самого факта использования. Если логика простая и не требует большого количества ресурсов — лучше обойтись без мемоизации. Вызов useMemo сам по себе потребляет немного ресурсов.

Пропущенные зависимости
Если указать не все зависимости, значение может быть рассчитано с устаревшими данными.

Неверно:

```python
const value = useMemo(() => compute(data), []); // data не указано

Верно:  
```python  
const value = useMemo(() => compute(data), \[data\]);
  1. Для надёжности можно использовать ESLint-плагин react-hooks, который предупредит о пропущенных зависимостях.

  2. Мемоизация с нестабильными ссылками
    Если одна из зависимостей сама изменяется при каждом рендере (например, () => {}), то 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 не вызовет лишний ререндер.

Как избежать ненужных пересчётов

  1. Следить за зависимостями. Указывай только те переменные, которые действительно влияют на результат.

  2. Избегай мемоизации тривиальных вычислений. Нет смысла мемоизировать x + 1, но есть смысл мемоизировать сортировку 10 000 записей.

  3. Не мемоизируй функции в useMemo. Если цель — сохранить функцию, используй useCallback.

  4. **Используй useMemo для props сложных компонентов, зависящих от больших данных.
    **

  5. Не оборачивай всё подряд. Мемоизация имеет смысл только там, где она реально предотвращает лишнюю работу и даёт выигрыш по производительности.

Вложенные 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, сложными операциями и динамически вычисляемыми значениями.