Ось готовий урок, створений у стилі Девіда Малана (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: "Після того, як ти намалюєш все на екрані, зроби ось цю побічну дію".
Три стани життя компонента:
- Mounting (Народження) 🌱
- Компонент вперше з'явився в DOM.
- Приклад: Завантажити дані з API, запустити таймер.
- Updating (Життя/Зміни) 🔄
- Змінилися пропси або стан (state).
- Приклад: Користувач змінив ID товару, треба довантажити нову інформацію.
- 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) і спробуйте:
- 🔹 Повтори: Створи компонент, який при завантаженні (
mount) робитьalert('Welcome!'). Переконайся, що це стається лише 1 раз. - 🔹 Динамічний заголовок: Створи компонент із полем вводу (
input). ВикористайuseEffect, щоб заголовок вкладки браузера (document.title) змінювався на те, що ти пишеш в інпуті.- Підказка: Залежність має бути стан інпута.
- 🔹 Window Resize: Напиши компонент, який показує поточну ширину вікна браузера.
- Додай слухач події
resizeвuseEffect. - Важливо: Не забудь видалити слухач у
return, інакше створиш витік пам'яті!
- Додай слухач події
- 🔹 Виправ помилку:
javascript useEffect(() => { const timer = setInterval(() => setCount(count + 1), 1000); // Чого тут не вистачає? }, []); // Чому count завжди показує дивне значення або 1? - 🔹 Міні-кейс: Реалізуй "відлік до вибуху". Компонент показує число 10. Щосекунди воно зменшується. Коли доходить до 0 — компонент пише "БУМ!" і зупиняє таймер.
5. 💡 Мислення як у розробника
Як думає Senior-розробник, дивлячись на useEffect?
-
"Синхронізація", а не просто "Життєвий цикл". Досвідчений розробник думає: "Мені треба синхронізувати мій компонент із зовнішньою системою (API, DOM, Timer)".
-
Пастка нескінченного циклу (Infinite Loop).
- Помилка новачка: Змінювати стан (
setCount) всерединіuseEffect, який залежить від цього ж стану ([count]). - Результат: Ефект змінює стан -> Рендер -> Ефект бачить зміну -> Змінює стан -> Рендер... ♾️
- Помилка новачка: Змінювати стан (
-
Завжди чисти за собою. Якщо ти підписався на щось (WebSocket, події, таймери) — ти зобов'язаний відписатися. Уяви, що твій компонент — це квартирант. Ніхто не любить квартирантів, які залишають сміття після виїзду.
-
exhaustive-deps (Правило лінтера). Якщо React каже вам: "Ти використав змінну X в ефекті, але не додав її в масив залежностей", — слухайте його. Ігнорування цього попередження — джерело 90% багів у React.
6. 🧩 Підсумок
Отже, що ми сьогодні розібрали?
- Ми навчилися втручатися в життя компонента за допомогою
useEffect. - Ми знаємо, що
[](порожній масив) означає "тільки при старті". - Ми знаємо, що масив
[prop]означає "при старті та при змініprop". - Ми зрозуміли, що функція
returnвсередині ефекту — це кнопка "Стоп" або "Прибирання".
Тепер ви вмієте робити запити до серверів, керувати таймерами та слухати браузерні події правильно! 🎉
🚀 Що далі?
У наступному уроці ми поговоримо про Custom Hooks. Ми візьмемо цю логіку з useEffect і сховаємо її у власну функцію, щоб наш код виглядав ще чистішим. Це як створити власний інструмент замість того, щоб щоразу збирати його з нуля.
А поки — практикуйтеся і не забувайте про масив залежностей! 😉