useEffect та життєвий цикл.useEffect: призначення та базовий синтаксис.useEffect.З попередньої лекції ми дізналися, що React-компонент в ідеалі працює за формулою: UI = f(State, Props). Дали йому дані — він намалював кнопки. Але що робити, якщо ці дані треба спочатку завантажити з сервера? Що робити, якщо ми хочемо запустити таймер зворотного відліку? Або якщо потрібно вручну звернутися до document.title і змінити назву вкладки в браузері?
Всі ці дії виходять за межі “просто намалювати UI”. В термінології програмування вони називаються Побічними ефектами (Side Effects).
Якщо ми спробуємо виконати запит до сервера безпосередньо в тілі компонента (класичній функції), ми випадково зламаємо весь додаток, адже функція буде запускатися знову і знову при кожній найменшій зміні.
Щоб виконувати побічні ефекти безпечно, синхронно з етапами життя компонента, розробники використовують наймогутніший (і найскладніший для новачків) хук — useEffect.
Мета цієї лекції — зрозуміти філософію “ефектів”, фази “Життєвого циклу” та навчитися робити API-запити без ризику покласти сервер безкінечними циклами.
React бере своє коріння з функціонального програмування. Одна з головних вимог React — кожен компонент повинен бути Чистою Функцією при рендерингу.
Чиста функція (Pure Function) — це функція, яка:
Math.random(), читання файлу).Все, що порушує ці правила, називається Побічним ефектом (Side Effect).
Але без побічних ефектів веб-додаток був би просто мертвою картинкою. Нам ПОТРІБНІ запити в інтернет та таймери.
Тому React каже: “Виконуйте ваші побічні ефекти, але робіть це у спеціальному місці”. Цим місцем є useEffect.
Як і людина, кожен React-компонент у браузері проходить через три головні фази життя:
Props або State. Компонент перераховується і перемальовує свою частину екрана. Ця фаза може повторюватися тисячі разів.if), і він назавжди видаляється з DOM.Давним-давно (у Класових компонентах) для кожної фази були окремі функції (componentDidMount, componentDidUpdate). Зараз useEffect дозволяє контролювати усі три фази одночасно.
useEffect: базовий синтаксисuseEffect викликається на верхньому рівні компонента. Він приймає два аргументи:
import { useEffect, useState } from "react";
function PageTitle() {
const [name, setName] = useState("Іван");
// Ефект: змінюємо заголовок вкладки браузера
useEffect(() => {
document.title = `Сторінка користувача ${name}`;
});
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
Якщо ми НЕ передамо другий аргумент (як у прикладі вище), цей ефект буде спрацьовувати спочатку при Монтуванні, а потім — ПІСЛЯ КОЖНОГО ОНОВЛЕННЯ компонента. (Кожна надрукована буква в input буде викликати цей ефект).
Масив залежностей — це масив [], який ми передаємо другим аргументом.
Це пульт керування ефектом. Ви ніби кажете React-у: “Будь ласка, запускай цю функцію знову, ТІЛЬКИ ЯКЩО змінилася хоча б одна з цих змінних”.
Існує рівно 3 конфігурації масиву залежностей:
1. Ефект без масиву взагалі (Використовується дуже рідко)
useEffect(() => {
// Виконається після ПЕРШОГО рендеру і після КОЖНОГО подальшого рендеру
});
2. Порожній масив [] (Використовується найчастіше для API)
useEffect(() => {
// Виконається ТІЛЬКИ ОДИН РАЗ (при Монтуванні).
// Ідеально підходить для початкового завантаження даних (fetch).
}, []);
3. Масив зі змінними [var1, var2] (Реактивний ефект)
useEffect(() => {
// Виконається при Монтуванні, а також щоразу, коли зміниться var1 або var2.
// Якщо і var1 і var2 залишилися незмінними - ефект ігнорується.
searchServer(query);
}, [query]);
Золоте правило лінтера: Якщо ви використовуєте всередині {тіла ефекту} будь-яку змінну (prop або state), ви зобов’язані вказати її у масиві залежностей. Якщо ви цього не зробите, ефект зафіксує старе, застаріле значення цієї змінної.
useEffectНайпопулярніше застосування useEffect — це зробити запит за інформацією до сервера (API) одразу після того, як сторінка завантажилася.
Оскільки useEffect не може бути напряму асинхронним (не можна написати useEffect(async () => {})), ми створюємо async-функцію всередині нього і одразу її викликаємо:
function UserList() {
// 1. Стан для збереження фінальних даних (порожній масив)
const [users, setUsers] = useState([]);
// 2. Стан для індикатора завантаження
const [isLoading, setIsLoading] = useState(true);
// 3. Сам ефект, який запуститься 1 раз при старті
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch("https://api.test/users");
const data = await response.json();
// Зберігаємо справжні дані в State
setUsers(data);
} catch (error) {
console.error("Помилка!", error);
} finally {
// В будь-якому випадку вимикаємо "крутилку"
setIsLoading(false);
}
};
fetchUsers();
}, []); // ПОРОЖНІЙ МАСИВ! Це гарантія, що запит не піде по колу
// Рендеринг UI
if (isLoading) return <h2>Завантаження даних...</h2>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Іноді наш Ефект залишає після себе “сміття” у системі.
Наприклад, при Монтуванні компонента-віджета ми запустили вічний таймер (через setInterval(..., 1000)). Коли користувач піде на іншу сторінку, компонент зникне (Демонтування).
АЛЕ таймер продовжить працювати (цокати кожну секунду) в пам’яті комп’ютера! Це називається “Витік пам’яті” (Memory Leak). Якщо користувач зайде і вийде зі сторінки 100 разів, запуститься 100 паралельних таймерів і браузер “впаде”.
Щоб прибрати за собою, useEffect може return (повернути) спеціальну функцію Очищення (Cleanup). React автоматично викличе цю функцію якраз перед Демонтуванням компонента, (а також перед кожним наступним перезапуском цього ж ефекту).
useEffect(() => {
console.log("Ефект: Ставимо таймер!");
// Запускаємо щось, що працює постійно
const timerId = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
// Функція очищення (Прибирання)
return () => {
console.log("Очищення: Зупиняємо старий таймер!");
clearInterval(timerId); // Вбиваємо попередній процес
};
}, []);
Ця техніка також використовується для відписки від WebSockets або скасування мережевих запитів (AbortController), якщо користувач натиснув кнопку “Назад”, не дочекавшись завантаження файлу.
Найжахливіший баг новачка — влаштувати DDoS атаку на сервер власноруч.
Це стається, коли ви змішуєте useEffect і useState, забувши про масив залежностей.
// ЯК НЕ ТРЕБА РОБИТИ (код-самовбивця):
function BadComponent() {
const [data, setData] = useState([]);
useEffect(() => {
fetch("/api/data").then((res) => setData(res.data));
}); // ❌ Забули поставити [] !!!
return <div>...</div>;
}
Чому це ламається (цикл смерті):
useEffect (без масиву).fetch і в кінці викликає setData(...).setData(...) змінює State.useEffect (бо масиву немає, значить треба пускати після кожного оновлення).useEffect знову робить fetch і викликає setData.useEffect! fetch! setData!
… І так 100 000 разів на секунду, поки браузер не зависне або сервер не заблокує вашу IP-адресу.Завжди використовуйте масив [] для API запитів!
useEffect.useEffect приймає два аргументи: функцію (що робити) та Масив Залежностей (коли це робити).[], ефект запуститься лише один раз (при Монтуванні). Це стандартна поведінка для завантаження даних fetch.useEffect не підтримує нативне async перед своєю основною callback-функцією. Асинхронні запити потрібно вкладати як внутрішні функції.useEffect повинен повертати іншу функцію (Cleanup), яка запуститься перед Демонтуванням.fetch) або запуск таймера setInterval вважаються Побічними Ефектами, що порушують чистоту?useEffect? Коли саме він запуститься?useEffect із порожнім масивом залежностей []? Як часто він виконується?useEffect( async () => {...}, [])? Як правильно робити асинхронні виклики всередині цього хука?return () => {}) є абсолютно необхідною для запобігання витоку пам’яті? Коли саме React її викличе?[] при виклику fetch (який закінчується командою setData) призводить до безкінечного циклу зависання браузера?