Модуль 17

Життєвий цикл компонента через хуки

Ось готовий урок, створений у стилі Девіда Малана (CS50), адаптований під тему React Hooks.


🎓 Урок: Життєвий цикл компонента через хуки (useEffect)

Привіт, друзі! 👋

Уявіть собі, що ви організовуєте вечірку. Ви не просто "існуєте" на ній. У вас є чіткі етапи: 1. Ви приходите і вмикаєте музику (початок). 2. Ви змінюєте плейлист, коли настрій гостей змінюється (процес). 3. Ви прибираєте за собою і вимикаєте світло, коли йдете (кінець).

У світі React компоненти живуть точно так само. Вони народжуються (Mounting), змінюються (Updating) і вмирають (Unmounting).

Але стривайте! ✋ Якщо компонент — це просто функція, яка повертає HTML (JSX), то куди вставити код, який має запуститися тільки один раз на початку? Або код, який має почистити сміття в кінці? Якщо написати його просто в тілі функції, він буде виконуватися при кожному перемальовуванні екрана. Це катастрофа для продуктивності! 💥

Сьогодні ми розберемося, як керувати часом у React за допомогою суперсили — хука useEffect.


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

Уявіть, що ви пишете чат. Коли користувач відкриває вікно чату, вам потрібно під’єднатися до сервера (WebSocket). Якщо ви просто напишете connectToServer() всередині компонента:

function Chat() {
  connectToServer(); // 😱 О ні!
  return <div>Chat...</div>;
}

...React буде підключатися до сервера тисячі разів на хвилину — щоразу, коли ви вводите букву в поле вводу або рухаєте мишкою. Ваш сервер впаде, а браузер зависне.

Питання до вас: Як сказати React: "Ей, зроби це тільки один раз, коли компонент з'являється на екрані, і більше не чіпай"?

Або інша ситуація: ви підписалися на події прокрутки сторінки (window.addEventListener). Якщо користувач закриє компонент, а ви не "відпишетесь", подія продовжить працювати у фоні, пожираючи пам'ять. Це називається Memory Leak (витік пам'яті).

Без розуміння життєвого циклу ваші додатки будуть повільними, глючними і "зомбованими" старими процесами.


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

Давайте розкладемо все по поличках. У класових компонентах (стара школа) для цього було три різні методи. У функціональних компонентах (сучасний React) у нас є один універсальний інструмент — useEffect.

useEffect — це ваш спосіб сказати React: "Після того, як ти намалюєш все на екрані, зроби ось цю побічну дію".

Три стани життя компонента:

  1. Mounting (Народження) 🌱
    • Компонент вперше з'явився в DOM.
    • Приклад: Завантажити дані з API, запустити таймер.
  2. Updating (Життя/Зміни) 🔄
    • Змінилися пропси або стан (state).
    • Приклад: Користувач змінив ID товару, треба довантажити нову інформацію.
  3. Unmounting (Смерть) 💀
    • Компонент видаляється з екрана.
    • Приклад: Зупинити таймер, скасувати запит, видалити слухач подій.

Як це працює "під капотом"?

Синтаксис виглядає так:

useEffect(() => {
  // 1. Код тут виконується після рендеру

  return () => {
    // 2. Код тут виконується перед зникненням (очищення)
  };
}, [ /* 3. Масив залежностей */ ]);

🔑 Ключ до розуміння — масив залежностей (третій пункт): * Якщо масиву немає взагалі 👉 ефект запускається кожен рендер (небезпечно!). * Якщо масив порожній [] 👉 ефект запускається тільки 1 раз при народженні (Mount). * Якщо в масиві є змінні [userId] 👉 ефект запускається при старті ТА щоразу, коли userId змінюється (Update).


3. 🧪 Приклади (від простого до реального)

Приклад 1: "Привіт, світ!" (Mounting)

Завдання: Вивести повідомлення в консоль лише один раз, коли компонент завантажився.

Що ви очікуєте побачити в консолі, якщо ми натиснемо кнопку, яка оновлює лічильник?

import { useState, useEffect } from 'react';

function Welcome() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("👋 Компонент змонтовано (Народився)!");
  }, []); // <--- Порожній масив = ТІЛЬКИ ОДИН РАЗ

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Клік {count}</button>
    </div>
  );
}

Пояснення: Навіть якщо ви клікаєте 100 разів і компонент перемальовується, console.log спрацює лише один раз. Чому? Бо масив залежностей [] порожній. React знає: "Тут нічого не змінюється, перезапускати не треба".


Приклад 2: Реагуємо на зміни (Updating)

Ситуація: У вас є профіль користувача. Коли змінюється userId, треба повідомити про це.

function UserProfile({ userId }) {
  useEffect(() => {
    console.log(`🔄 Завантажую дані для користувача ID: ${userId}`);
  }, [userId]); // <--- Слідкуємо за userId

  return <h1>Профіль користувача #{userId}</h1>;
}

Логіка: 1. Перший рендер (userId = 1) -> Ефект спрацював. 2. Пропси змінилися (userId = 2) -> React бачить, що [1] відрізняється від [2], і запускає ефект знову.


Приклад 3: Прибираємо за собою (Unmounting)

Найскладніше, але найважливіше. Таймер. Якщо ми запустимо setInterval, він тікатиме вічно, навіть якщо ми перейдемо на іншу сторінку.

function Timer() {
  useEffect(() => {
    // Дія при старті
    const intervalId = setInterval(() => {
      console.log('⏰ Тік-так...');
    }, 1000);

    // Дія при смерті (Cleanup function)
    return () => {
      console.log('🛑 Зупиняємо таймер!');
      clearInterval(intervalId);
    };
  }, []);

  return <div>Таймер працює (дивіться консоль)</div>;
}

Що відбувається: Функція, яку ви повертаєте з useEffect (return () => ...), — це інструкція для прибирання. React викличе її перед тим, як видалити компонент, або перед наступним запуском ефекту (якщо залежності змінилися).


4. 🛠 Практична частина

Час кодити! Відкрийте редактор (або CodeSandbox) і спробуйте:

  1. 🔹 Повтори: Створи компонент, який при завантаженні (mount) робить alert('Welcome!'). Переконайся, що це стається лише 1 раз.
  2. 🔹 Динамічний заголовок: Створи компонент із полем вводу (input). Використай useEffect, щоб заголовок вкладки браузера (document.title) змінювався на те, що ти пишеш в інпуті.
    • Підказка: Залежність має бути стан інпута.
  3. 🔹 Window Resize: Напиши компонент, який показує поточну ширину вікна браузера.
    • Додай слухач події resize в useEffect.
    • Важливо: Не забудь видалити слухач у return, інакше створиш витік пам'яті!
  4. 🔹 Виправ помилку: javascript useEffect(() => { const timer = setInterval(() => setCount(count + 1), 1000); // Чого тут не вистачає? }, []); // Чому count завжди показує дивне значення або 1?
  5. 🔹 Міні-кейс: Реалізуй "відлік до вибуху". Компонент показує число 10. Щосекунди воно зменшується. Коли доходить до 0 — компонент пише "БУМ!" і зупиняє таймер.

5. 💡 Мислення як у розробника

Як думає Senior-розробник, дивлячись на useEffect?

  1. "Синхронізація", а не просто "Життєвий цикл". Досвідчений розробник думає: "Мені треба синхронізувати мій компонент із зовнішньою системою (API, DOM, Timer)".

  2. Пастка нескінченного циклу (Infinite Loop).

    • Помилка новачка: Змінювати стан (setCount) всередині useEffect, який залежить від цього ж стану ([count]).
    • Результат: Ефект змінює стан -> Рендер -> Ефект бачить зміну -> Змінює стан -> Рендер... ♾️
  3. Завжди чисти за собою. Якщо ти підписався на щось (WebSocket, події, таймери) — ти зобов'язаний відписатися. Уяви, що твій компонент — це квартирант. Ніхто не любить квартирантів, які залишають сміття після виїзду.

  4. exhaustive-deps (Правило лінтера). Якщо React каже вам: "Ти використав змінну X в ефекті, але не додав її в масив залежностей", — слухайте його. Ігнорування цього попередження — джерело 90% багів у React.


6. 🧩 Підсумок

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

  • Ми навчилися втручатися в життя компонента за допомогою useEffect.
  • Ми знаємо, що [] (порожній масив) означає "тільки при старті".
  • Ми знаємо, що масив [prop] означає "при старті та при зміні prop".
  • Ми зрозуміли, що функція return всередині ефекту — це кнопка "Стоп" або "Прибирання".

Тепер ви вмієте робити запити до серверів, керувати таймерами та слухати браузерні події правильно! 🎉

🚀 Що далі? У наступному уроці ми поговоримо про Custom Hooks. Ми візьмемо цю логіку з useEffect і сховаємо її у власну функцію, щоб наш код виглядав ще чистішим. Це як створити власний інструмент замість того, щоб щоразу збирати його з нуля.

А поки — практикуйтеся і не забувайте про масив залежностей! 😉