Ось готовий урок, згенерований за твоїм майстер-промптом.
🎓 CS50 Style: useCallback та useMemo на практиці
Привіт, друзі! Це CS50 (умовно 😉), і сьогодні ми зануримося в одну з найбільш міфологізованих тем React.
Уявіть, що ви зайшли в Google Docs, натиснули одну клавішу, а ваш ноутбук "завис" на секунду, бо браузер вирішив перерахувати кількість слів у всьому документі, перевірити правопис у всьому тексті й заново завантажити всі картинки.
Звучить жахливо, правда?
Але саме це часто роблять ваші React-додатки, якщо ви не розумієте, коли треба сказати React: "Ей, стоп! Ми це вже рахували. Візьми готовий результат".
Сьогодні ми розберемося з інструментами, які рятують продуктивність: useMemo та useCallback.
1. 🔥 Вступ: Проблема та мотивація
Уявіть, що ви — шеф-кухар у дуже популярному ресторані. До вас приходить офіціант і каже: "Клієнт замовив омлет". Ви готуєте омлет. Це швидко.
А тепер уявіть, що клієнт замовив качку по-пекінськи, яку треба маринувати добу і запікати 3 години. Ви це зробили. Через хвилину офіціант повертається: "Клієнт попросив ще серветку". Ви даєте серветку, але за правилами "дивної кухні React", ви викидаєте готову качку і починаєте готувати нову. Просто тому, що офіціант до вас звернувся знову.
Риторичне питання: Ви б довго протримались на такій кухні?
У React, коли компонент оновлюється (ре-рендер), запускається весь код всередині нього згори донизу.
* Якщо ви там рахуєте числа Фібоначчі — вони рахуються заново.
* Якщо ви створюєте функцію handleClick — вона створюється заново (нова адреса в пам’яті!).
Навіщо нам useMemo і useCallback?
Щоб сказати React: "Качка вже готова, не готуй нову, просто дай серветку". Це економить ресурси процесора і робить додаток плавним.
2. 🧠 Теоретична база (Без нудних лекцій)
Давайте заглянемо "під капот". У JavaScript є одна підступна річ: посилання (references).
Що треба зрозуміти інтуїтивно:
- Примітиви рівні за значенням:
5 === 5(True). - Об’єкти та функції — ні:
{} === {}(False).function(){} === function(){}(False).
Щоразу, коли ваш компонент ре-рендериться, всі функції всередині нього створюються нові. Для React це виглядає так, ніби ви передали абсолютно нову функцію, навіть якщо код всередині ідентичний.
Визначення "на пальцях":
-
useMemo(Запам’ятай РЕЗУЛЬТАТ)- Це як кеш. Ви кажете: "Ось складна формула. Порахуй її один раз і запам’ятай число. Перерахуй тільки якщо зміниться
xабоy". - Аналогія: Ви записали результат множення на дошці, щоб не множити в стовпчик щоразу.
- Це як кеш. Ви кажете: "Ось складна формула. Порахуй її один раз і запам’ятай число. Перерахуй тільки якщо зміниться
-
useCallback(Запам’ятай ФУНКЦІЮ)- Це як заморозка самої "інструкції". Ви кажете: "Збережи посилання на цю функцію і не створюй нову під час наступного рендеру".
- Аналогія: Ви не пишете новий рецепт омлету щоразу, ви дістаєте старий записник із тим самим рецептом.
3. 🧪 Приклади (Від простого до реального)
Приклад 1: Проблема важких обчислень
У нас є компонент. У ньому є лічильник (швидкий) і "важке обчислення" (повільне).
import React, { useState } from 'react';
function HeavyComponent() {
const [count, setCount] = useState(0);
const [darkTheme, setDarkTheme] = useState(false);
// 🔴 Уявіть, що це гальмує систему
const heavyNumber = slowFunction(count);
function slowFunction(num) {
console.log('🐌 Викликаю важку функцію...');
for (let i = 0; i < 1000000000; i++) {} // Імітація затримки
return num * 2;
}
return (
<div style={{ background: darkTheme ? '#333' : '#FFF' }}>
<h1>Число: {heavyNumber}</h1>
<button onClick={() => setCount(count + 1)}>Змінити число</button>
{/* Питання: Що буде, коли я натисну цю кнопку? */}
<button onClick={() => setDarkTheme(!darkTheme)}>Змінити тему</button>
</div>
);
}
Питання до студента: Я натискаю "Змінити тему". Колір фону має просто змінитись. Чи запуститься slowFunction?
...
Відповідь: ТАК! І інтерфейс "залипне". Бо змінився стан (darkTheme) $\rightarrow$ ре-рендер $\rightarrow$ функція slowFunction запустилась знову. Це марнотратство.
Приклад 2: Рішення з useMemo
Виправляємо це.
import React, { useState, useMemo } from 'react';
// ... всередині компонента
const heavyNumber = useMemo(() => {
return slowFunction(count);
}, [count]); // 👈 Масив залежностей
Чому це працює: Ми сказали React: "Перераховуй це ТІЛЬКИ якщо змінився count". Коли ми клацаємо тему, count той самий, тому React бере число з "кишені" (пам'яті) миттєво.
Приклад 3: Підступний useEffect і useCallback
Це класична пастка новачків.
import React, { useState, useEffect } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
// Ця функція перестворюється при кожному натисканні клавіші!
const fetchResults = () => {
console.log('Fetching data for:', query);
};
useEffect(() => {
fetchResults();
console.log('Effect triggered!');
}, [fetchResults]); // 🔴 Упс! Нескінченний цикл або зайві виклики
return <input onChange={e => setQuery(e.target.value)} />;
}
Що тут не так?
Оскільки fetchResults — це об’єкт (функція), при кожному рендері він новий. useEffect дивиться: "О, нова функція fetchResults? Значить, треба запустити ефект!".
Рішення з useCallback:
import React, { useState, useEffect, useCallback } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
// ✅ Тепер функція стабільна між рендерами, поки не зміниться query
const fetchResults = useCallback(() => {
console.log('Fetching data for:', query);
}, [query]);
useEffect(() => {
fetchResults();
console.log('Effect triggered correcty');
}, [fetchResults]); // Тепер це безпечно
return <input onChange={e => setQuery(e.target.value)} />;
}
4. 🛠 Практична частина
Час кодити! Відкрийте свій редактор або CodeSandbox.
🔹 Завдання 1: Оптимізація списку
Створіть компонент, який фільтрує великий масив імен (наприклад, 1000 імен).
* Додайте input для пошуку.
* Додайте кнопку, яка просто змінює лічильник "Like" (не пов’язаний зі списком).
* Задача: Зробіть так, щоб при натисканні "Like" фільтрація масиву не відбувалася заново. (Підказка: useMemo).
🔹 Завдання 2: Referential Equality
Створіть батьківський компонент і дочірній Child.
* Батько передає функцію onClick дитині.
* Огорніть дитину в React.memo(Child).
* Проблема: Дитина все одно ре-рендериться, коли батько оновлюється.
* Задача: Використати useCallback для функції в батька, щоб React.memo нарешті запрацював.
🔹 Завдання 3: Міні-кейс "Корзина товарів"
У вас є масив товарів у кошику: [{ id: 1, price: 100 }, { id: 2, price: 50 }].
* Напишіть обчислення загальної суми.
* Додайте можливість застосувати промокод (просто чекбокс), який дає -10%.
* Використайте useMemo так, щоб сума товарів (без знижки) не перераховувалася, коли ми просто клацаємо чекбокс промокоду (припустимо, що сумування — це важка операція).
🔹 Питання "А що, якщо..."
А що, якщо масив залежностей [] порожній у useMemo? Коли відбудеться обчислення?
(Відповідь спробуйте перевірити кодом).
5. 💡 Мислення як у розробника
Ось де ми відрізняємо новачків від профі.
❌ Помилка новачка:
Огортати ВСЕ в useMemo і useCallback.
"Я оптимізую кожну змінну і кожну функцію!"
🧠 Як думає Senior:
1. Ціна оптимізації: Самі хуки useMemo та useCallback теж витрачають ресурси. React треба виділити пам'ять, порівняти залежності... Для простих операцій (як 2 + 2) це дорожче, ніж просто порахувати заново.
2. Коли використовувати:
* Referential Integrity: Коли я передаю функцію/об'єкт у useEffect або у React.memo компонент. (Тут це обов'язково!).
* Expensive Calculations: Коли я дійсно бачу лаги або обробляю тисячі елементів.
Порада: Спочатку напишіть код, щоб він працював. Якщо бачите просідання FPS або зайві рендери (використовуйте React DevTools Profiler) — тоді додавайте ці хуки. Не займайтеся передчасною оптимізацією.
6. 🧩 Підсумок
Отже, що ми сьогодні забрали з собою?
- React за замовчуванням забуває все при кожному рендері.
useMemo— це ваш "блокнот" для готових розрахунків (кешуємо значення).useCallback— це спосіб зберегти ідентичність функції (щоб React не думав, що це нова функція).- Ми використовуємо їх, щоб уникнути важких перерахунків та стабілізувати
useEffectабоReact.memo.
Тепер ви вмієте робити ваші додатки не тільки функціональними, а й швидкими. Ви контролюєте потік даних, а не він вас.
🔜 У наступному уроці: Ми дізнаємося, як винести логіку (і ці ж самі useMemo/Callback) у власні Custom Hooks, щоб ваш код виглядав як магія.
А поки що — це був useCallback та useMemo. Практикуйтесь!