useMemo кэширует результат вычисления между рендерами. React выполняет вашу функцию при первом рендере, сохраняет результат и возвращает тот же результат при следующих рендерах, пока не изменится одно из значений в массиве зависимостей.
Откройте Console и несколько раз переключите тему: ничего не выводится, и переключение мгновенное. Смените вкладку, и появится filtering for active с короткой паузой, потому что tab это зависимость. Теперь замените строку с useMemo на const visible = filterTodos(todos, tab); и снова переключите тему: каждый клик выводит сообщение и подвисает на 100 мс.
Синтаксис
const cachedValue = useMemo(calculateValue, dependencies);
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
calculateValueэто функция без аргументов, которая возвращает значение для кэширования. React вызывает её во время рендера.dependenciesэто список всех значений из компонента, которые читает функция: пропсы, состояние и переменные, объявленные в теле компонента.- При каждом рендере React сравнивает каждую зависимость с её значением из предыдущего рендера через
Object.is. Если все совпадают, он возвращает кэшированное значение, не вызывая функцию. Если хоть одна отличается, он вызывает функцию и кэширует новый результат.
useMemo это хук, поэтому он подчиняется правилам хуков: вызывайте его на верхнем уровне компонента, а не внутри цикла или условия.
Зависимости должны быть полными
Пропустите значение в массиве, и кэш продолжит выдавать результат, вычисленный из его старого значения. Здесь итог читает qty, но в списке только price:
Нажмите Quantity: оно станет 2, но итог останется 10, и ничего не выведется. Нажмите Price, и итог прыгнет до 22, потому что только тогда вычисление выполнится снова, уже читая текущий qty. Добавьте qty в массив, и обе кнопки будут обновлять итог. В настоящем проекте эту ошибку ловит правило линтера react-hooks/exhaustive-deps.
Обратная ошибка это зависимость, которая новая при каждом рендере, например объект, созданный в теле компонента. Она никогда не совпадает с предыдущим значением, поэтому кэш никогда не используется:
const filters = { tab, query }; // a new object every render
const visible = useMemo(() => applyFilters(todos, filters), [todos, filters]); // recomputes every time
// List the primitive values instead
const visible = useMemo(() => applyFilters(todos, { tab, query }), [todos, tab, query]);
Стабильный объект для дочернего компонента с memo
Вторая причина взяться за useMemo не имеет отношения к медленным вычислениям. Дочерний компонент, обёрнутый в memo, пропускает рендер, только когда каждый пропс такой же, как в прошлый раз. Литерал объекта в теле компонента при каждом рендере новый объект, поэтому он сводит memo на нет, даже если содержимое не изменилось. useMemo сохраняет тот же объект, пока не изменятся его входные данные.
Пишите в поле заметки, и Chart молчит, а Switch unit выводит один рендер. Замените вызов useMemo на простой const options = { unit, max: unit === 'km' ? 100 : 62 };, и каждое нажатие клавиши в заметке будет выводить render Chart.
То же касается эффекта, который зависит от объекта. Если эффект указывает options в зависимостях, а options пересоздаётся при каждом рендере, эффект выполняется после каждого рендера. useMemo это исправляет, но часто проще создать объект внутри эффекта и зависеть от примитивных значений:
useEffect(() => {
const options = { serverUrl, roomId };
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
Когда useMemo не нужен
Большинство вычислений в компоненте дешёвые: сложить числа, соединить две строки, отфильтровать пару десятков элементов. Их кэширование почти ничего не экономит: React всё равно хранит значение и сравнивает зависимости при каждом рендере, а код становится сложнее читать.
// No useMemo needed
const fullName = first + ' ' + last;
const doneCount = todos.filter((t) => t.done).length;
Чтобы узнать, медленное ли что-то, измерьте время. Оберните вычисление в console.time('filter') и console.timeEnd('filter'), выполните взаимодействие и посмотрите на число. Грубый ориентир: работа, которая на вашей машине занимает миллисекунду или больше, кандидат на мемоизацию, учитывая, что устройства пользователей могут быть медленнее. Прежде чем браться за useMemo, проверьте также, место ли этой работе в рендере вообще: данные, которые никогда не меняются, можно вычислить один раз вне компонента, а состояние, которое использует только небольшая часть страницы, можно перенести в компонент поменьше (см. рендеринг).
useMemo или useEffect для производных значений
Частый шаблон: вычислить значение в эффекте и сохранить его в состоянии:
// Two renders per change, and the first one has stale data
const [visible, setVisible] = useState([]);
useEffect(() => {
setVisible(filterTodos(todos, tab));
}, [todos, tab]);
// One render, always in sync
const visible = useMemo(() => filterTodos(todos, tab), [todos, tab]);
Версия с эффектом один раз рендерится со старым visible, затем выполняет эффект, устанавливает состояние и рендерится снова. Это лишний рендер, и в первом список не соответствует вкладке. Вычисление во время рендера через useMemo (или вообще без хука, если работа дешёвая) даёт правильное значение в том же рендере. Эффекты нужны для синхронизации с тем, что вне React, а не для вывода одной части состояния из другой.
Подсказка для производительности, а не гарантия
useMemo сообщает React, что результат можно переиспользовать. Он не обещает, что так и будет. В обычной работе React хранит кэшированное значение, но может его выбросить, например когда вы редактируете файл в разработке или когда компонент приостанавливается при первом монтировании. Пишите код так, чтобы он оставался правильным, если вычисление выполнится снова: функция должна быть чистой, без побочных эффектов вроде запросов, подписок или изменения внешних переменных. Побочным эффектам место в обработчике события или эффекте.
В разработке под StrictMode React вызывает вашу функцию вычисления дважды, чтобы помочь заметить нечистый код. Превью здесь работает как продакшен-сборка, поэтому каждый лог filtering for вы увидите один раз.
useMemo и React Compiler
React Compiler 1.0 это инструмент сборки, который анализирует ваши компоненты и автоматически добавляет мемоизацию, кэшируя вычисления и JSX там, где помогли бы useMemo, useCallback и memo. В проекте, который его использует, вы будете писать их вручную реже. Ручной useMemo по-прежнему работает вместе с компилятором, остаётся инструментом, когда компилятор не настроен, и встречается в большей части существующего кода на React.
Часто задаваемые вопросы
Что делает useMemo в React?
Он вызывает вашу функцию вычисления при первом рендере, сохраняет результат и при следующих рендерах возвращает сохранённый результат, пока каждое значение в массиве зависимостей такое же, как в прошлый раз (сравнение через Object.is).
Когда использовать useMemo?
Когда вычисление заметно медленное, а его входные данные редко меняются, или когда объекту или массиву нужно сохранять одну и ту же ссылку между рендерами, потому что от неё зависит дочерний компонент с memo или эффект. Для дешёвой работы он не нужен.
Чем useMemo отличается от useCallback?
useMemo кэширует значение, которое возвращает ваша функция. useCallback кэширует саму функцию. useCallback(fn, deps) то же самое, что useMemo(() => fn, deps).
Гарантирует ли useMemo, что значение никогда не будет пересчитано?
Нет. Это подсказка для производительности. React в некоторых случаях может выбросить кэшированное значение, например в разработке при редактировании файла, поэтому код должен работать правильно, даже если вычисление выполнится снова.
Нужен ли useMemo с React Compiler?
React Compiler может добавлять мемоизацию автоматически, если ваша сборка его использует. Без компилятора или в случаях, которые он пропускает, ручной useMemo работает так же и по-прежнему распространён в существующем коде.