Модуль 23

useCallback і useMemo на практиці

Ось готовий урок, згенерований за твоїм майстер-промптом.


🎓 CS50 Style: useCallback та useMemo на практиці

Привіт, друзі! Це CS50 (умовно 😉), і сьогодні ми зануримося в одну з найбільш міфологізованих тем React.

Уявіть, що ви зайшли в Google Docs, натиснули одну клавішу, а ваш ноутбук "завис" на секунду, бо браузер вирішив перерахувати кількість слів у всьому документі, перевірити правопис у всьому тексті й заново завантажити всі картинки.

Звучить жахливо, правда?

Але саме це часто роблять ваші React-додатки, якщо ви не розумієте, коли треба сказати React: "Ей, стоп! Ми це вже рахували. Візьми готовий результат".

Сьогодні ми розберемося з інструментами, які рятують продуктивність: useMemo та useCallback.


1. 🔥 Вступ: Проблема та мотивація

Уявіть, що ви — шеф-кухар у дуже популярному ресторані. До вас приходить офіціант і каже: "Клієнт замовив омлет". Ви готуєте омлет. Це швидко.

А тепер уявіть, що клієнт замовив качку по-пекінськи, яку треба маринувати добу і запікати 3 години. Ви це зробили. Через хвилину офіціант повертається: "Клієнт попросив ще серветку". Ви даєте серветку, але за правилами "дивної кухні React", ви викидаєте готову качку і починаєте готувати нову. Просто тому, що офіціант до вас звернувся знову.

Риторичне питання: Ви б довго протримались на такій кухні?

У React, коли компонент оновлюється (ре-рендер), запускається весь код всередині нього згори донизу. * Якщо ви там рахуєте числа Фібоначчі — вони рахуються заново. * Якщо ви створюєте функцію handleClick — вона створюється заново (нова адреса в пам’яті!).

Навіщо нам useMemo і useCallback? Щоб сказати React: "Качка вже готова, не готуй нову, просто дай серветку". Це економить ресурси процесора і робить додаток плавним.


2. 🧠 Теоретична база (Без нудних лекцій)

Давайте заглянемо "під капот". У JavaScript є одна підступна річ: посилання (references).

Що треба зрозуміти інтуїтивно:

  1. Примітиви рівні за значенням: 5 === 5 (True).
  2. Об’єкти та функції — ні: {} === {} (False). function(){} === function(){} (False).

Щоразу, коли ваш компонент ре-рендериться, всі функції всередині нього створюються нові. Для React це виглядає так, ніби ви передали абсолютно нову функцію, навіть якщо код всередині ідентичний.

Визначення "на пальцях":

  1. useMemo (Запам’ятай РЕЗУЛЬТАТ)

    • Це як кеш. Ви кажете: "Ось складна формула. Порахуй її один раз і запам’ятай число. Перерахуй тільки якщо зміниться x або y".
    • Аналогія: Ви записали результат множення на дошці, щоб не множити в стовпчик щоразу.
  2. 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. 🧩 Підсумок

Отже, що ми сьогодні забрали з собою?

  1. React за замовчуванням забуває все при кожному рендері.
  2. useMemo — це ваш "блокнот" для готових розрахунків (кешуємо значення).
  3. useCallback — це спосіб зберегти ідентичність функції (щоб React не думав, що це нова функція).
  4. Ми використовуємо їх, щоб уникнути важких перерахунків та стабілізувати useEffect або React.memo.

Тепер ви вмієте робити ваші додатки не тільки функціональними, а й швидкими. Ви контролюєте потік даних, а не він вас.

🔜 У наступному уроці: Ми дізнаємося, як винести логіку (і ці ж самі useMemo/Callback) у власні Custom Hooks, щоб ваш код виглядав як магія.

А поки що — це був useCallback та useMemo. Практикуйтесь!