useEffect: Асинхронна природа виконання (після Commit Phase).useLayoutEffect vs useEffect: Синхронні мутації DOM та проблема “мерехтіння” екрану (Flickering).React пропагує написання компонентів як “Чистих функцій” (Pure Functions). Компонент повинен отримати props та state і повернути JSX. На етапі обчислення цього JSX (Render Phase) категорично заборонено змінювати зовнішній світ (робити запити на сервер, підписуватись на події window, взаємодіяти з localStorage). Всі ці зовнішні взаємодії називаються Побічними ефектами (Side Effects).
Щоб легально виконувати сайд-ефекти в екосистемі функціональних компонентів і синхронізувати React зі “світом поза React”, був створений хук useEffect. Розуміння його роботи — це розуміння того, як React поєднує синхронний рендеринг із асинхронним середовищем браузера.
Кожен компонент в екосистемі React проходить три фундаментальні стадії існування:
useState) або контексту (useContext), функція компонента викликається знову. React генерує новий JSX, порівнює його зі старим (Reconciliation/Diffing) і точково оновлює DOM. Ця фаза може відбуватись сотні разів на секунду.show && <Modal/>, де show = false). DOM вузол фізично знищується.У старих класових компонентах розробники змушені були “розривати” логіку одного процесу (наприклад, підключення до чату) між трьома методами: componentDidMount, componentDidUpdate та componentWillUnmount. Хук useEffect елегантно об’єднує всі три фази в єдиний логічний блок (Closures/Замикання).
Що таке Побічний Ефект (Side Effect)? Це будь-яка операція, стан якої виходить за межі повернення JSX-розмітки або мутує систему на рівні операційної системи браузера.
fetch / axios).document.title = "Привіт").window.addEventListener('scroll', ...)).setTimeout, setInterval).navigator.geolocation, localStorage).Чому їх не можна писати прямо в тілі компонента?
const BadComponent = () => {
// КРИТИЧНА ПОМИЛКА: Це виконається при КОЖНОМУ рендері.
// Якщо є стан, що оновлюється часто (наприклад, набір тексту),
// ви відправите тисячі запитів на бекенд (DDoS атака на власні сервери).
fetch("https://api.example.com/data");
return <div />;
};
Рендеринг має залишатися чистою математичною обрахунковою операцією. Тому будь-який код, що змінює систему, обертають у useEffect.
useEffect: Асинхронна природа виконанняФункція, яку ви передаєте всередину useEffect, виконується ПІСЛЯ того, як React завершив рендеринг і фізично відмалював зміни в DOM (Commit Phase). Це зроблено навмисно: щоб “важкі” запити або таймери не блокували можливість браузера показати користувачу свіжий інтерфейс (Non-blocking rendering).
const AnatomyExample = () => {
const [count, setCount] = useState(0);
// Крок 2: Після оновлення DOM, браузер "відмалює" екран і ЛИШЕ ПОТІМ виконає цей код
useEffect(() => {
document.title = `Кліків: ${count}`;
});
// Крок 1: Цей JSX миттєво генерується і відправляється в DOM
return <button onClick={() => setCount((c) => c + 1)}>Клік</button>;
};
Другим аргументом useEffect приймає масив залежностей. Цей масив визначає КОЛИ React повинен повторно викликати ефект.
Механіка React проста: під час рендеру №2 він бере старий масив залежностей (з рендеру №1) і новий масив, і порівнює кожен елемент попарно за допомогою алгоритму Object.is() (тобто перевіряє строгу рівність — Referential Equality).
Ось чотири можливі патерни:
Без масиву взагалі:
useEffect(() => {
/* ... */
});
Фаза: Виконується після Першого рендеру І ПІСЛЯ КОЖНОГО наступого оновлення. Зазвичай уникається, оскільки часто спричиняє безкінечні цикли.
Порожній масив []:
useEffect(() => {
/* ... */
}, []);
Фаза: Виконується ТІЛЬКИ ОДИН РАЗ, одразу після першого монтування компонента (Mount). Еквівалент старого componentDidMount. Ідеально для завантаження початкових даних.
Масив з примітивами [id, user.name]:
useEffect(() => {
fetchUser(id);
}, [id]);
Фаза: Ефект виконається спочатку після Mount, а потім — тільки якщо значення змінної id змінилося між рендерами (1 !== 2).
Пастка з об’єктами (Reference Bug):
const config = { type: "admin" }; // Створюється НОВЕ посилання в пам'яті при кожному рендері
// ПОМИЛКА: Нескінченний цикл, бо Object.is(oldConfig, newConfig) ЗАВЖДИ false.
useEffect(() => {
fetchItems();
}, [config]);
Рішення: Або винести config за межі компонента, або мемоїзувати об’єкт (useMemo), або “деструктуризувати” примітиви в масив: [config.type].
Наслідком того, що компоненти постійно оновлюються або розмонтовуються, є те, що “старі” побічні ефекти повинні бути зупинені, інакше виникне виток пам’яті (Memory Leak).
Якщо ваш ефект повертає функцію, React вважає її Функцією Очищення (Cleanup).
const Tracker = ({ userId }) => {
useEffect(() => {
// 1. Створюємо підписку на чат (Ефект)
const connection = ChatAPI.subscribeTo(userId);
// Функція очищення (РЕТУРН)
return () => {
// 2. Вона виконається ПЕРЕД наступним викликом ефекту,
// АБО коли компонент розмонтується (Unmount)
connection.unsubscribe();
};
}, [userId]); // перепідключитися, якщо змінився userId
};
Чому це критично при роботі з API (Race Conditions):
Уявіть, що користувач натиснув на Профіль №1. Пішов запит, він “висить” 3 секунди. Користувач не дочекався і натиснув Профіль №2 (пішов запит №2). Раптом бекенд повертає Профіль №1, і на екрані для Користувача 2 рендериться фото Користувача 1.
Функція очищення з використанням AbortController (DOM стандарту) “вбиває” застарілі запити “в польоті”.
В React 18 була представлена нова парадигма: React може навмисно розмотувати і монтувати компоненти “в тіні” під час конкурентного рендерингу (наприклад, при кешуванні сторінок).
Щоб підготувати розробників до цього, в середовищі розробки (Development) <React.StrictMode> форсує Подвійний виклик ефектів (Double Mount).
Життєвий цикл у DevMode виглядає так:
Mount -> Effect -> Unmount (виймає Cleanup) -> Mount -> Effect.
Це не баг, а “стрес-тест”. Якщо ваш додаток ламається під час подвійного виклику useEffect (наприклад, дублюються повідомлення в консолі/базі, відео грає поверх іншого відео), це означає, що ви забули написати коректну Функцію Очищення. У продакшені (Production) цей подвійний виклик відключено.
useLayoutEffect vs useEffectІнколи асинхронна природа useEffect є проблемою.
Наприклад, ви хочете прочитати висоту щойно відрендереного блоку і миттєво перерендерити екран, підставивши скроллбар поруч.
Якщо ви зробите це в useEffect, користувач побачить, як блок відмалювався “просто так”, а за мить (кадр) він “стрибає”, зміщуючись під скроллбар. Це називається Мерехтіння (Flickering).
Для таких суворо синхронних випадків існує useLayoutEffect. Цей хук спрацьовує:
useLayoutEffect (блокуючи браузеру малювання пікселів на екран)useEffect (асинхронно).Архітектурне правило: Віддавайте перевагу useEffect у 99% випадків, оскільки він не блокує малювання екрану. Використовуйте useLayoutEffect виключно тоді, коли ви стикаєтеся з видимими візуальними ривками (flickering) при маніпуляціях з DOM.
useEffect — це міст між чистою декларативною парадигмою React (Render Phase) та імперативним асинхронним середовищем браузера / мережі.Object.is).StrictMode в React 18 — це інструмент для автоматичного виявлення неробочих або відсутніх Cleanup-функцій через симуляцію миттєвого розмонтування-монтування.useEffect та блокуючий, синхронний useLayoutEffect, залишаючи останній лише для специфічних візуальних мутацій розмірів DOM.addEventListener архітектурно заборонено виконувати в тілі компонента (до return JSX), і їх необхідно обгорнути в useEffect?useEffect за наявності порожнього масиву залежностей [] порівняно з відсутністю масиву взагалі. Який життєвий цикл стимулюється?StrictMode у розробці та навіщо він навмисно форсує життєвий цикл ефекту “Монтування -> Очищення -> Монтування” перед тим, як показати результат розробнику.useEffect та застосує useLayoutEffect? Що відбувається з процесом візуального малювання (Paint) у браузері в момент виклику обох хуків?