Пошук уроків, статей та іншого контенту
Зрозумієте, як useMemo кешує результати обчислень і коли його застосування справді покращує продуктивність.
useMemouseMemo — це React-хук для мемоізації, тобто кешування результату обчислення між рендерами компонента.
const cachedValue = useMemo(calculateValue, dependencies);calculateValue — функція без аргументів, яка повертає результат;
dependencies — масив значень, від яких залежить результат;
cachedValue
React виконує функцію під час першого рендера, а потім повторно використовує збережене значення, доки залежності не змінилися.
import { useMemo } from 'react';
function ProductList({ products, query }) {
const filteredProducts = useMemo(() => {
return products.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase())
);
}, [products, query]);
return (
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}У цьому прикладі фільтрація виконується повторно лише тоді, коли змінюються products або query.
Компонент React повторно виконується щоразу, коли змінюється його стан або властивості. Через це повторно виконуються і всі обчислення всередині компонента.
Для простих операцій це не проблема:
const fullName = `${firstName} ${lastName}`;Але обчислення може бути дорогим, якщо воно:
обробляє велику колекцію;
виконує фільтрацію, сортування або групування;
запускає складний алгоритм;
виконується під час багатьох непов'язаних ререндерів.
У такій ситуації useMemo дозволяє не повторювати обчислення без потреби.
Уявімо сторінку каталогу. Користувач змінює пошуковий запит, категорію або тему інтерфейсу. Зміна теми не впливає на список товарів, тому фільтрацію можна не запускати повторно.
import { useMemo, useState } from 'react';
const products = Array.from({ length: 10_000 }, (_, index) => ({
id: index + 1,
name: `Товар ${index + 1}`,
category: index % 2 === 0 ? 'books' : 'electronics',
price: 100 + (index % 20) * 50,
}));
export default function App() {
const [query, setQuery] = useState('');
const [category, setCategory] = useState('all');
const [darkTheme, setDarkTheme] = useState(false);
const visibleProducts = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
return products
.filter((product) => {
const matchesQuery = product.name
.toLowerCase()
.includes(normalizedQuery);
const matchesCategory =
category === 'all' || product.category === category;
return matchesQuery && matchesCategory;
})
.sort((firstProduct, secondProduct) => {
return firstProduct.price - secondProduct.price;
});
}, [query, category]);
return (
<main
style={{
minHeight: '100vh',
padding: 24,
background: darkTheme ? '#222' : '#fff',
color: darkTheme ? '#fff' : '#222',
}}
>
<h1>Каталог товарів</h1>
<label>
Пошук:
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Введіть назву"
/>
</label>
<label>
Категорія:
<select
value={category}
onChange={(event) => setCategory(event.target.value)}
>
<option value="all">Усі</option>
<option value="books">Книги</option>
<option value="electronics">Електроніка</option>
</select>
</label>
<button onClick={() => setDarkTheme((theme) => !theme)}>
Змінити тему
</button>
<p>Знайдено товарів: {visibleProducts.length}</p>
<ul>
{visibleProducts.slice(0, 100).map((product) => (
<li key={product.id}>
{product.name} — {product.price} грн
</li>
))}
</ul>
</main>
);
}Залежності містять лише query і category, оскільки саме вони впливають на результат фільтрації.
Коли змінюється darkTheme, компонент рендериться знову, але React може використати вже обчислений visibleProducts.
React порівнює кожну залежність із попереднім значенням. Для цього використовується порівняння, подібне до Object.is.
Для примітивних значень це зазвичай очевидно:
const result = useMemo(() => {
return query.toLowerCase();
}, [query]);Якщо query має те саме значення, обчислення не повторюється.
Об'єкти, масиви та функції порівнюються за посиланням, а не за вмістом:
const options = { category: 'books' };
const result = useMemo(() => {
return filterProducts(products, options);
}, [products, options]);Якщо options створюється під час кожного рендера, його посилання щоразу нове. Тому React вважатиме, що залежність змінилася, і повторить обчислення.
У такому випадку об'єкт можна створити всередині функції мемоізації:
const result = useMemo(() => {
const options = { category };
return filterProducts(products, options);
}, [products, category]);Тепер залежністю є примітивне значення category, а не новий об'єкт options.
useMemo не зупиняє рендер компонентаВажливо розрізняти дві операції:
повторне виконання компонента;
повторне виконання конкретного обчислення.
useMemo не забороняє компоненту рендеритися. Він лише може повернути попередній результат замість повторного запуску функції обчислення.
const result = useMemo(() => {
return expensiveCalculation(data);
}, [data]);Якщо компонент повторно відрендерився через іншу зміну, useMemo перевірить залежності. Якщо data не змінилося, функція expensiveCalculation не буде викликана повторно.
useMemo справді доречнийЗастосовуйте useMemo, коли одночасно виконуються такі умови:
обчислення може бути відчутно дорогим;
компонент рендериться часто;
залежності обчислення змінюються рідше, ніж відбуваються ререндери;
результат обчислення використовується в поточному рендері.
Типові приклади:
const filteredItems = useMemo(
() => items.filter((item) => item.isVisible),
[items]
);const sortedItems = useMemo(
() => [...items].sort(compareItems),
[items]
);const statistics = useMemo(
() => calculateStatistics(records),
[records]
);Перед додаванням мемоізації варто переконатися, що саме це обчислення є проблемним. Для цього можна використати профілювання або вимірювання часу виконання.
useMemo не потрібенНе кожне обчислення потребує кешування:
const total = price * quantity;
const title = `${firstName} ${lastName}`;Для таких операцій витрати на сам useMemo можуть бути більшими за вигоду:
const total = useMemo(() => price * quantity, [price, quantity]);Такий код складніший і зазвичай не дає практичного покращення продуктивності.
useMemo також не слід додавати автоматично до кожного масиву, об'єкта або рядка. Мемоізація має бути оптимізацією для конкретної проблеми, а не обов'язковим стилем написання компонентів.
Окрім економії обчислень, useMemo може зберігати те саме посилання на об'єкт або масив. Це важливо, коли значення передається дочірньому компоненту або використовується як залежність іншого хука.
const chartData = useMemo(() => {
return records.map((record) => ({
label: record.date,
value: record.amount,
}));
}, [records]);Якщо records не змінився, chartData матиме те саме посилання між рендерами.
Проте саме по собі це не забороняє дочірньому компоненту рендеритися. Для пропуску рендера дочірнього компонента потрібні додаткові умови, зокрема його оптимізація за властивостями. У межах useMemo важливо розуміти лише те, що хук стабілізує результат конкретного обчислення.
У масиві залежностей потрібно вказати всі значення з компонента, які читаються всередині обчислення та можуть змінюватися.
const filteredProducts = useMemo(() => {
return products.filter((product) =>
product.name.includes(query)
);
}, [products, query]);Тут потрібні обидві залежності:
products, бо обчислення читає список товарів;
query, бо обчислення читає пошуковий запит.
Якщо пропустити залежність, результат може стати застарілим:
// Неправильно: query використовується, але відсутній у залежностях
const filteredProducts = useMemo(() => {
return products.filter((product) =>
product.name.includes(query)
);
}, [products]);Таке обчислення не оновиться після зміни query.
useMemo не є сховищем даних і не призначений для збереження стану. Кешований результат потрібно розглядати як оптимізацію.
Компонент має залишатися коректним, навіть якщо обчислення виконується повторно. Не варто покладатися на useMemo для:
виконання побічних ефектів;
збереження важливих даних між рендерами;
логіки, яка працює лише завдяки тому, що функція не запускається повторно.
Функція всередині useMemo повинна виконувати обчислення та повертати значення. Побічні ефекти слід розміщувати в інших відповідних механізмах React.
const doubled = useMemo(() => count * 2, [count]);Для простої арифметики це зазвичай зайва складність. Спочатку використовуйте звичайне обчислення.
Якщо значення використовується всередині функції, але відсутнє в залежностях, результат може не відповідати поточним даним.
// Неправильно
const result = useMemo(() => calculate(items, multiplier), [items]);Правильніше:
const result = useMemo(
() => calculate(items, multiplier),
[items, multiplier]
);const filter = { category };
const products = useMemo(() => {
return filterProducts(allProducts, filter);
}, [allProducts, filter]);filter — новий об'єкт на кожному рендері, тому мемоізація втрачає сенс. Краще залежати від category і створити об'єкт усередині обчислення.
Метод sort змінює початковий масив. Якщо items належить пропсам або стану, не змінюйте його напряму:
const sortedItems = useMemo(() => {
return [...items].sort(compareItems);
}, [items]);Створення копії гарантує, що початковий масив залишиться незмінним.
useMemo для побічних ефектів// Неправильно
useMemo(() => {
localStorage.setItem('theme', theme);
}, [theme]);useMemo призначений для обчислення значення, а не для побічних ефектів. Його функція має повертати результат обчислення.
useMemo кешує результат обчислення між рендерами.
Обчислення повторюється, коли змінюється хоча б одна залежність.
Хук не зупиняє рендер компонента, а лише уникає повторного запуску конкретної функції.
Усі значення, від яких залежить результат, потрібно вказати в масиві залежностей.
Об'єкти, масиви та функції порівнюються за посиланням.
Для простих обчислень useMemo зазвичай не потрібен.
Використовуйте його для вимірюваних проблем продуктивності, а не автоматично.
useMemo — оптимізація, а не механізм збереження стану чи виконання побічних ефектів.