nmk

Лекція №8 (2 години). Життєвий цикл компонентів та хук useEffect: Управління побічними ефектами

План лекції

  1. Життєвий цикл компонента: Монтування (Mount), Оновлення (Update), Розмонтування (Unmount).
  2. Парадигма побічних ефектів (Side Effects) у декларативному UI: Чому сайд-ефектам не місце в рендері.
  3. Анатомія хука useEffect: Асинхронна природа виконання (після Commit Phase).
  4. Масив залежностей (Dependency Array): Строга рівність (Referential Equality) та механізм тригерів.
  5. Функція очищення (Cleanup Function): Захист від витоку пам’яті (Memory Leaks) та Race Conditions.
  6. Життєвий цикл хуків у React 18: Вплив Strict Mode на подвійне монтування (Double Mount).
  7. useLayoutEffect vs useEffect: Синхронні мутації DOM та проблема “мерехтіння” екрану (Flickering).

Перелік умовних скорочень

Вступ

React пропагує написання компонентів як “Чистих функцій” (Pure Functions). Компонент повинен отримати props та state і повернути JSX. На етапі обчислення цього JSX (Render Phase) категорично заборонено змінювати зовнішній світ (робити запити на сервер, підписуватись на події window, взаємодіяти з localStorage). Всі ці зовнішні взаємодії називаються Побічними ефектами (Side Effects). Щоб легально виконувати сайд-ефекти в екосистемі функціональних компонентів і синхронізувати React зі “світом поза React”, був створений хук useEffect. Розуміння його роботи — це розуміння того, як React поєднує синхронний рендеринг із асинхронним середовищем браузера.


1. Життєвий цикл компонента: Mount, Update, Unmount

Кожен компонент в екосистемі React проходить три фундаментальні стадії існування:

  1. Монтування (Mounting): Компонент вперше викликається, React будує для нього Fiber Node, і результат рендеру (JSX) фізично додається до реального DOM дерева браузера.
  2. Оновлення (Updating): Внаслідок зміни пропсів, локального стану (useState) або контексту (useContext), функція компонента викликається знову. React генерує новий JSX, порівнює його зі старим (Reconciliation/Diffing) і точково оновлює DOM. Ця фаза може відбуватись сотні разів на секунду.
  3. Розмонтування (Unmounting): Компонент видаляється з екрану (наприклад, користувач перейшов на іншу сторінку або спрацював умовний рендеринг show && <Modal/>, де show = false). DOM вузол фізично знищується.

У старих класових компонентах розробники змушені були “розривати” логіку одного процесу (наприклад, підключення до чату) між трьома методами: componentDidMount, componentDidUpdate та componentWillUnmount. Хук useEffect елегантно об’єднує всі три фази в єдиний логічний блок (Closures/Замикання).


2. Парадигма побічних ефектів у декларативному UI

Що таке Побічний Ефект (Side Effect)? Це будь-яка операція, стан якої виходить за межі повернення JSX-розмітки або мутує систему на рівні операційної системи браузера.

Чому їх не можна писати прямо в тілі компонента?

const BadComponent = () => {
  // КРИТИЧНА ПОМИЛКА: Це виконається при КОЖНОМУ рендері.
  // Якщо є стан, що оновлюється часто (наприклад, набір тексту),
  // ви відправите тисячі запитів на бекенд (DDoS атака на власні сервери).
  fetch("https://api.example.com/data");

  return <div />;
};

Рендеринг має залишатися чистою математичною обрахунковою операцією. Тому будь-який код, що змінює систему, обертають у useEffect.


3. Анатомія хука 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>;
};

4. Масив залежностей (Dependency Array): Механізм тригерів

Другим аргументом useEffect приймає масив залежностей. Цей масив визначає КОЛИ React повинен повторно викликати ефект.

Механіка React проста: під час рендеру №2 він бере старий масив залежностей (з рендеру №1) і новий масив, і порівнює кожен елемент попарно за допомогою алгоритму Object.is() (тобто перевіряє строгу рівність — Referential Equality).

Ось чотири можливі патерни:

  1. Без масиву взагалі:

    useEffect(() => {
      /* ... */
    });
    

    Фаза: Виконується після Першого рендеру І ПІСЛЯ КОЖНОГО наступого оновлення. Зазвичай уникається, оскільки часто спричиняє безкінечні цикли.

  2. Порожній масив []:

    useEffect(() => {
      /* ... */
    }, []);
    

    Фаза: Виконується ТІЛЬКИ ОДИН РАЗ, одразу після першого монтування компонента (Mount). Еквівалент старого componentDidMount. Ідеально для завантаження початкових даних.

  3. Масив з примітивами [id, user.name]:

    useEffect(() => {
      fetchUser(id);
    }, [id]);
    

    Фаза: Ефект виконається спочатку після Mount, а потім — тільки якщо значення змінної id змінилося між рендерами (1 !== 2).

  4. Пастка з об’єктами (Reference Bug):

    const config = { type: "admin" }; // Створюється НОВЕ посилання в пам'яті при кожному рендері
    // ПОМИЛКА: Нескінченний цикл, бо Object.is(oldConfig, newConfig) ЗАВЖДИ false.
    useEffect(() => {
      fetchItems();
    }, [config]);
    

    Рішення: Або винести config за межі компонента, або мемоїзувати об’єкт (useMemo), або “деструктуризувати” примітиви в масив: [config.type].


5. Функція очищення (Cleanup Function): Захист від Memory Leaks

Наслідком того, що компоненти постійно оновлюються або розмонтовуються, є те, що “старі” побічні ефекти повинні бути зупинені, інакше виникне виток пам’яті (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 стандарту) “вбиває” застарілі запити “в польоті”.


6. Життєвий цикл хуків у React 18: Вплив Strict Mode

В React 18 була представлена нова парадигма: React може навмисно розмотувати і монтувати компоненти “в тіні” під час конкурентного рендерингу (наприклад, при кешуванні сторінок).

Щоб підготувати розробників до цього, в середовищі розробки (Development) <React.StrictMode> форсує Подвійний виклик ефектів (Double Mount). Життєвий цикл у DevMode виглядає так: Mount -> Effect -> Unmount (виймає Cleanup) -> Mount -> Effect.

Це не баг, а “стрес-тест”. Якщо ваш додаток ламається під час подвійного виклику useEffect (наприклад, дублюються повідомлення в консолі/базі, відео грає поверх іншого відео), це означає, що ви забули написати коректну Функцію Очищення. У продакшені (Production) цей подвійний виклик відключено.


7. useLayoutEffect vs useEffect

Інколи асинхронна природа useEffect є проблемою. Наприклад, ви хочете прочитати висоту щойно відрендереного блоку і миттєво перерендерити екран, підставивши скроллбар поруч. Якщо ви зробите це в useEffect, користувач побачить, як блок відмалювався “просто так”, а за мить (кадр) він “стрибає”, зміщуючись під скроллбар. Це називається Мерехтіння (Flickering).

Для таких суворо синхронних випадків існує useLayoutEffect. Цей хук спрацьовує:

  1. React обчислив зміни (Render)
  2. React вставив вузли в реальний DOM.
  3. ТУТ викликається useLayoutEffect (блокуючи браузеру малювання пікселів на екран)
  4. Браузер малює пікселі (Paint).
  5. Викликається useEffect (асинхронно).

Архітектурне правило: Віддавайте перевагу useEffect у 99% випадків, оскільки він не блокує малювання екрану. Використовуйте useLayoutEffect виключно тоді, коли ви стикаєтеся з видимими візуальними ривками (flickering) при маніпуляціях з DOM.


Висновки

  1. useEffect — це міст між чистою декларативною парадигмою React (Render Phase) та імперативним асинхронним середовищем браузера / мережі.
  2. Реактивність залежить від Dependency Array (масиву залежностей). Він диктує рушію React умову, коли ефект потрібно запустити наново (на базі алгоритму Object.is).
  3. Код, що відкриває “потік” даних (таймери, підписки, Websocket з’єднання, HTTP-запити), зобов’язаний повертати Cleanup-функцію для закриття цього потоку і запобігання витокам пам’яті.
  4. Вплив StrictMode в React 18 — це інструмент для автоматичного виявлення неробочих або відсутніх Cleanup-функцій через симуляцію миттєвого розмонтування-монтування.
  5. Розробник має розрізняти асинхронний, “безпечний” для потоку малювання браузером useEffect та блокуючий, синхронний useLayoutEffect, залишаючи останній лише для специфічних візуальних мутацій розмірів DOM.

Джерела

  1. Офіційна документація React. “Synchronizing with Effects”. URL: https://react.dev/learn/synchronizing-with-effects
  2. React Docs. “Lifecycle of Reactive Effects”. URL: https://react.dev/learn/lifecycle-of-reactive-effects
  3. “A Complete Guide to useEffect”. Dan Abramov’s Overreacted Blog. URL: https://overreacted.io/a-complete-guide-to-useeffect/
  4. MDN Web Docs. “Memory management in JS (Garbage Collection)”. URL: https://developer.mozilla.org/
  5. React Docs. “How to handle effect dependencies”. URL: https://react.dev/learn/removing-effect-dependencies
  6. MDN Web API. “AbortController (Cancel Fetch)”. URL: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
  7. “React 18: Fixing the double render in Strict mode”. LogRocket Blog.
  8. Kyle Simpson. “You Don’t Know JS: Async & Performance” (В контексті розуміння Event Loop та Microtasks, на які спирається useEffect).

Запитання для самоперевірки

  1. З точки зору виконання коду браузером, чому HTTP-запити та підписки addEventListener архітектурно заборонено виконувати в тілі компонента (до return JSX), і їх необхідно обгорнути в useEffect?
  2. Опишіть поведінку useEffect за наявності порожнього масиву залежностей [] порівняно з відсутністю масиву взагалі. Який життєвий цикл стимулюється?
  3. Що таке механізм Cleanup-функції і яким чином вона допомагає вирішити проблему Race Condition (Стан гонитви) при виконанні мережевих запитів з повільним інтернетом?
  4. Поясніть причину використання React StrictMode у розробці та навіщо він навмисно форсує життєвий цикл ефекту “Монтування -> Очищення -> Монтування” перед тим, як показати результат розробнику.
  5. За яких екстремальних умов інженер прийме рішення відмовитися від useEffect та застосує useLayoutEffect? Що відбувається з процесом візуального малювання (Paint) у браузері в момент виклику обох хуків?