Пошук уроків, статей та іншого контенту
Застосуємо useMemo та useCallback для кешування обчислень і функцій без зайвої мемоізації.
useMemo і useCallbackПід час кожного рендерингу компонента React повторно виконує його функцію. Це означає, що:
обчислення всередині компонента запускаються знову;
функції, оголошені всередині компонента, створюються заново;
дочірній компонент може отримати нові посилання на значення або функції, навіть якщо їхній вміст не змінився.
У більшості випадків це не проблема. Але якщо обчислення складне або дочірній компонент оптимізований за допомогою React.memo, повторні обчислення можуть бути зайвими.
Для таких випадків використовують:
useMemo — кешує результат обчислення;
useCallback — кешує посилання на функцію.
Мемоізація — це оптимізація продуктивності, а не спосіб виправити помилки в логіці компонента.
useMemo: кешування обчисленого значенняСинтаксис:
const cachedValue = useMemo(() => {
return calculateValue(data);
}, [data]);React повторно виконає функцію всередині useMemo, лише якщо змінилася хоча б одна залежність.
Наприклад:
const filteredProducts = useMemo(() => {
return products.filter((product) =>
product.name.toLowerCase().includes(search.toLowerCase())
);
}, [products, search]);Тут filteredProducts буде обчислено повторно, якщо зміняться products або search. Зміна інших станів компонента не призведе до повторної фільтрації.
useMemouseMemo кешує значення, повернуте функцією:
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);У цьому прикладі кешується число total.
useMemo не забороняє React повторно виконувати компонент. Він лише дозволяє не виконувати конкретне обчислення без потреби.
useCallback: кешування функціїuseCallback кешує саме посилання на функцію:
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);Без useCallback під час кожного рендерингу створювалася б нова функція:
const handleSelect = (id) => {
setSelectedId(id);
};Функції в JavaScript — це об'єкти. Навіть якщо дві функції мають однаковий код, вони є різними об'єктами:
(() => {}) === (() => {}); // falseТому для дочірнього компонента нова функція є зміною пропса.
Розглянемо список завдань. У компоненті є:
пошук за назвою;
перемикач теми;
фільтрація завдань;
дочірній компонент, оптимізований через React.memo.
import { memo, useCallback, useMemo, useState } from "react";
const initialTodos = [
{ id: 1, title: "Вивчити компоненти React", completed: true },
{ id: 2, title: "Розібратися з хуками", completed: false },
{ id: 3, title: "Створити навчальний проєкт", completed: false },
{ id: 4, title: "Написати тести", completed: false },
];
const TodoList = memo(function TodoList({ todos, onToggle }) {
console.log("TodoList: рендеринг");
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.title}
</label>
</li>
))}
</ul>
);
});
export default function App() {
const [todos, setTodos] = useState(initialTodos);
const [search, setSearch] = useState("");
const [darkTheme, setDarkTheme] = useState(false);
const visibleTodos = useMemo(() => {
console.log("Фільтрація завдань");
const normalizedSearch = search.trim().toLowerCase();
return todos.filter((todo) =>
todo.title.toLowerCase().includes(normalizedSearch)
);
}, [todos, search]);
const handleToggle = useCallback((id) => {
setTodos((currentTodos) =>
currentTodos.map((todo) =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
}, []);
return (
<main
style={{
minHeight: "100vh",
padding: "24px",
backgroundColor: darkTheme ? "#222" : "#fff",
color: darkTheme ? "#fff" : "#222",
}}
>
<h1>Список завдань</h1>
<label>
Пошук:
<input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Введіть назву завдання"
/>
</label>
<button onClick={() => setDarkTheme((current) => !current)}>
Змінити тему
</button>
<p>
Знайдено завдань: {visibleTodos.length}
</p>
<TodoList todos={visibleTodos} onToggle={handleToggle} />
</main>
);
}Під час зміни теми компонент App рендериться повторно.
Однак:
todos не змінився.
search не змінився.
useMemo повертає вже обчислений масив visibleTodos.
handleToggle зберігає те саме посилання завдяки useCallback.
TodoList, обгорнутий у memo, отримує ті самі пропси й може пропустити повторний рендеринг.
Під час зміни пошукового запиту:
змінюється search;
useMemo повторно фільтрує список;
visibleTodos отримує нове значення;
TodoList рендериться, бо його список змінився.
Під час перемикання стану завдання:
змінюється todos;
useMemo повторно обчислює відфільтрований список;
TodoList рендериться з актуальними даними.
Масив залежностей має містити всі значення, які використовуються всередині функції та можуть змінитися.
Наприклад:
const result = useMemo(() => {
return items.filter((item) => item.category === selectedCategory);
}, [items, selectedCategory]);Залежності тут правильні, тому що обчислення використовує items і selectedCategory.
Якщо пропустити залежність:
const result = useMemo(() => {
return items.filter((item) => item.category === selectedCategory);
}, [items]);після зміни selectedCategory React може повернути застарілий результат.
Те саме стосується useCallback:
const handleSave = useCallback(() => {
saveDocument(documentId, content);
}, [documentId, content]);Функція має оновитися, якщо змінюється documentId або content.
У практичному прикладі handleToggle не має залежності від todos:
const handleToggle = useCallback((id) => {
setTodos((currentTodos) =>
currentTodos.map((todo) =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
}, []);Це можливо завдяки функціональному оновленню стану. React передає в callback актуальне значення currentTodos.
Альтернативний варіант вимагав би додати todos у залежності:
const handleToggle = useCallback(
(id) => {
setTodos(
todos.map((todo) =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
},
[todos]
);У такому випадку функція створюватиметься заново після кожної зміни todos. Це не завжди помилка, але функціональний запис часто дає стабільніше посилання на callback.
useMemouseMemo доречний, коли:
обчислення справді складне;
результат передається в компонент із React.memo;
результат використовується як залежність іншого хука;
обчислення виконується на великих масивах або структурах даних.
Типові приклади:
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.price - b.price);
}, [items]);const statistics = useMemo(() => {
return {
total: orders.length,
completed: orders.filter((order) => order.completed).length,
};
}, [orders]);Не потрібно використовувати useMemo для кожної простої операції:
const fullName = useMemo(() => `${firstName} ${lastName}`, [
firstName,
lastName,
]);Для такого короткого обчислення мемоізація зазвичай складніша за саме обчислення:
const fullName = `${firstName} ${lastName}`;useCallbackuseCallback особливо корисний, коли:
функція передається в дочірній компонент із React.memo;
функція використовується в масиві залежностей іншого хука;
стабільне посилання на функцію потрібне для конкретної оптимізації.
Наприклад:
const MemoizedButton = memo(function MemoizedButton({ onClick }) {
return <button onClick={onClick}>Зберегти</button>;
});
function Form() {
const [value, setValue] = useState("");
const handleSave = useCallback(() => {
console.log("Збережено:", value);
}, [value]);
return <MemoizedButton onClick={handleSave} />;
}Якщо handleSave оголошувати без useCallback, MemoizedButton отримуватиме нову функцію під час кожного рендерингу батьківського компонента.
Водночас сам по собі useCallback не запобігає рендерингу компонента. Він лише зберігає посилання на функцію. Для пропуску рендерингу дочірній компонент має враховувати стабільність пропсів, наприклад через React.memo.
useMemo і useCallback не замінюють useStateЦі хуки не зберігають дані як джерело істини й не призначені для керування станом.
Неправильний підхід:
const count = useMemo(() => 0, []);Значення не зміниться, і цей код не є заміною useState.
Для стану потрібно використовувати:
const [count, setCount] = useState(0);useMemo кешує похідне значення, а useCallback кешує функцію. Вони не повинні використовуватися для зберігання стану компонента.
Не кожен масив, об'єкт або callback потребує мемоізації. useMemo і useCallback теж мають власну вартість: React повинен зберігати попереднє значення та перевіряти залежності.
Спочатку варто знайти реальне проблемне місце, а потім додавати оптимізацію.
Якщо callback або обчислення використовує змінне значення, воно має бути в масиві залежностей.
const handleSearch = useCallback(() => {
console.log(search);
}, [search]);Пропуск search може призвести до роботи із застарілим значенням.
useCallback прискорить будь-який кодuseCallback не робить саму функцію швидшою. Він лише допомагає зберегти її посилання між рендерами.
Метод sort змінює початковий масив. Якщо сортування виконується в useMemo, спочатку створіть копію:
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.price - b.price);
}, [items]);Так ви не змінюєте стан або пропси напряму.
useMemo для виправлення логікиЯкщо результат неправильний, спочатку потрібно виправити обчислення та залежності. useMemo не гарантує правильність і не повинен використовуватися як спосіб приховати помилки в стані.
Практичний підхід може бути таким:
Написати просту й зрозумілу реалізацію.
Перевірити, чи справді є зайві рендеринги або повільні обчислення.
Знайти конкретне обчислення або пропс, який спричиняє проблему.
Додати useMemo для дорогого обчисленого значення.
Додати useCallback, якщо стабільність функції потрібна мемоізованому дочірньому компоненту.
Перевірити результат після оптимізації.
useMemo кешує результат обчислення.
useCallback кешує посилання на функцію.
Залежності визначають, коли кеш потрібно оновити.
useCallback найчастіше має сенс разом із React.memo.
Мемоізація потрібна для конкретних проблем продуктивності, а не автоматично для кожного значення.
Функціональне оновлення стану допомагає зменшити кількість залежностей callback-функцій.
Кешовані значення не замінюють стан і не виправляють помилки в логіці.