Як працює useMemo і навіщо він потрібен
useMemo - це хук React, який кешує обчислене значення між рендерами і перераховує його лише тоді, коли змінюються вказані залежності.
Теорія
TL;DR
- Уяви кухаря, який готує дорогий соус один раз за зміну і використовує його далі, поки ключові інгредієнти не змінились.
- Без
useMemoкожен рендер запускає обчислення знову, навіть якщо вхідні дані ті самі. - React зберігає результат і порівнює залежності через
Object.is. Якщо нічого не змінилось, повертає закешоване значення. - Використовуй, коли обчислення займає більше ~5ms і залежності стабільні більшість рендерів.
- Функції -
useCallback, значення -useMemo.
Швидкий приклад
import { useMemo, useState } from 'react';
const TodoList = ({ todos }) => {
const [filter, setFilter] = useState('all');
// Без useMemo: фільтрація на КОЖНОМУ рендері, навіть якщо filter не змінився
// const visible = todos.filter(t => t.status === filter);
const visible = useMemo(
() => todos.filter(t => t.status === filter),
[todos, filter] // перераховуємо тільки при зміні цих значень
);
return <ul>{visible.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
};Якщо батьківський компонент рендериться повторно, але filter і todos не змінились, useMemo повертає закешований масив. Фільтрація не відбувається.
Головна різниця
Без useMemo важкі функції запускаються на кожному рендері незалежно від того, чи змінились вхідні дані. Але є ще одна проблема: кожен рендер створює нове посилання на об'єкт або масив, що ламає рівність за посиланням для дочірніх компонентів і запускає їхні рендери теж. useMemo зберігає попередній результат у вузлі fiber компонента, порівнює кожну залежність через Object.is і пропускає callback, якщо нічого не змінилось. Один виклик рятує і від зайвих обчислень, і від каскадних рендерів нижче по дереву.
Коли використовувати
- Важка фільтрація, сортування або
reduceпо великих масивах (1 000+ елементів) -useMemo. - Похідний стан, який залишається стабільним більшість рендерів -
useMemo. - Об'єкт або масив, що передається як проп до компонента з
React.memo-useMemo. - Обчислення менше 1ms - просто напиши вираз inline.
- Залежності змінюються на кожному рендері -
useMemoтут не допоможе, кеш ніколи не спрацює.
Як це працює всередині
React зберігає кожен результат useMemo у зв'язаному списку memoizedState на вузлі fiber компонента. При першому рендері запускається mountMemo - зберігаються і значення, і масив залежностей. При повторному рендері запускається updateMemo: React проходить по залежностях через Object.is (це функція areHookInputsEqual у вихідному коді React) і якщо всі збігаються, повертає закешоване значення без виклику callback. Якщо хоча б одна залежність змінилась, callback запускається і результат зберігається.
Два моменти, які варто знати. По-перше, useMemo виконується синхронно під час фази рендеру, тому async-callback всередині не підтримується. По-друге, кеш не постійний. React може його видалити при нестачі пам'яті або коли fiber відкидається при розмонтуванні. Це підказка для оптимізації, а не гарантоване сховище.
useMemo vs useCallback
useMemo | useCallback | |
|---|---|---|
| Повертає | Значення | Функцію |
| Сценарій | Обчислені дані, об'єкти, масиви | Обробники подій, callbacks |
| Приклад | useMemo(() => a + b, [a, b]) | useCallback(() => doX(), [x]) |
| Коли пропустити | Дешеве обчислення | Callback не передається дочірнім компонентам |
Типові помилки
Нестабільний об'єкт у залежностях
// Перераховує кожен рендер - { key: 'value' } це нове посилання щоразу
const value = useMemo(() => heavyCalc(items), [items, { key: 'value' }]);
// Виправлення: передай примітив напряму
const value = useMemo(() => heavyCalc(items), [items, config.key]);Мемоізація дешевих операцій
// Накладні витрати хука більші за економію
const doubled = useMemo(() => a * 2, [a]);
// Просто напиши inline
const doubled = a * 2;Мутація мемоізованого значення
const list = useMemo(() => items.filter(isActive), [items]);
list.push(newItem); // Псує кеш - React очікує, що callback чистий
// Виправлення: створи новий масив
const withNew = [...list, newItem];Відсутні залежності
// userId змінюється, але useMemo ніколи не перераховує через порожній deps
const result = useMemo(() => fetchData(userId), []);
// Виправлення: додай userId до залежностей
const result = useMemo(() => fetchData(userId), [userId]);Каскад вкладених useMemo
const data1 = useMemo(() => heavy1(input), [input]);
const data2 = useMemo(() => heavy2(data1), [data1]); // data1 змінився - data2 теж перераховується
// Виправлення: об'єднай в один useMemo для всього пайплайну
const { data1, data2 } = useMemo(() => {
const d1 = heavy1(input);
return { data1: d1, data2: heavy2(d1) };
}, [input]);В продакшені я бачив, як такий каскадний патерн знищував продуктивність дашбордів із 4-5 похідними станами ланцюжком. Об'єднання в один useMemo скорочувало час рендеру на 60%.
Де зустрічається в реальних проектах
- TanStack Query мемоізує результати запитів, щоб стабільні дані не запускали зайві рендери нижче по дереву.
- Redux Toolkit's
createSelectorіз бібліотеки Reselect реалізує ту саму ідею, що йuseMemo, але на рівні стору. - Next.js використовує мемоізовані похідні метадані при обробці
getStaticProps. React.memo+useMemo- стандартний підхід:React.memoпропускає рендер дочірнього компонента, аuseMemoтримає пропси стабільними за посиланням, щоб пропуск реально спрацьовував.
Питання на співбесіді
Q: Яка часова складність порівняння залежностей у useMemo?
A: O(n), де n - кількість залежностей. React проходить по кожній через Object.is. Тому масив залежностей краще тримати коротким, до 5 елементів.
Q: Чи блокує useMemo рендеринг?
A: Так. Callback виконується синхронно під час фази рендеру. Якщо обчислення справді важке і блокує UI, useMemo не вирішить проблему - треба виносити роботу з головного потоку.
Q: У чому різниця між useMemo і React.memo?
A: useMemo кешує значення всередині одного компонента. React.memo обгортає компонент і пропускає його рендер, якщо пропси не змінились. Вони вирішують різні проблеми, але добре доповнюють одне одного.
Q: Що трапляється, якщо залежність - це об'єкт Date?
A: Object.is порівнює за посиланням. new Date() на кожному рендері - це новий об'єкт, тому порівняння завжди провалюється і useMemo завжди перераховує. Використовуй примітив - числову мітку часу або ISO-рядок.
Q: (Senior) Як concurrent mode впливає на кеш useMemo?
A: У React 18 низькопріоритетна робота може бути перервана, а fiber відкинутий. Коли fiber відкидається, його memoizedState втрачається і при перезапуску useMemo обчислює з нуля. Для некритичних обчислень обгорни оновлення стану в startTransition, щоб React знав, що може відкласти цю роботу.
Приклади
Базовий: фільтрація списку товарів
import { useMemo, useState } from 'react';
const ProductList = ({ products }) => {
const [search, setSearch] = useState('');
const filtered = useMemo(
() => products.filter(p => p.name.toLowerCase().includes(search.toLowerCase())),
[products, search]
);
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
<ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
</>
);
};filtered перераховується тільки при зміні products або search. Введення тексту в input запускає перерахунок. Рендер батьківського компонента з тими самими пропсами - ні.
Середній: обчислення статистики в todo-додатку
const TodoStats = ({ todos }) => {
const { activeCount, completedCount } = useMemo(() => {
let active = 0;
let completed = 0;
todos.forEach(todo => {
if (todo.completed) completed++;
else active++;
});
return { activeCount: active, completedCount: completed };
}, [todos]);
return <p>{activeCount} активних, {completedCount} завершених</p>;
};Об'єкт, повернутий із useMemo, залишається тим самим посиланням між рендерами, поки todos не змінюється. Це важливо, якщо TodoStats обгорнутий у React.memo - без цього новий об'єкт щоразу ламав би пропуск рендеру.
Просунутий: нестабільний об'єкт у залежностях
// Це не працює - config це новий об'єкт на кожному рендері батька
const BadChart = ({ data, config }) => {
const processed = useMemo(
() => data.map(item => ({ ...item, color: config.theme })),
[data, config] // config !== prev config, навіть якщо theme не змінився
);
return <Chart data={processed} />;
};
// Виправлення - деструктуруй примітив, який реально потрібен
const GoodChart = ({ data, config }) => {
const { theme } = config;
const processed = useMemo(
() => data.map(item => ({ ...item, color: theme })),
[data, theme] // порівняння примітивів працює коректно
);
return <Chart data={processed} />;
};Цей edge case спотикає навіть досвідчених розробників. Лінтер не завжди його ловить, бо config технічно використовується всередині callback. Виправлення - деструктурувати на початку і передавати до масиву залежностей тільки примітиви.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.