React хоче, щоб ваші компоненти були передбачуваними. Надайте йому той самий стан (state) і пропси (props), і він має відображати той самий інтерфейс (UI) щоразу. Але більшість реальних застосунків не можуть вижити всередині цієї «бульбашки». Їм потрібно взаємодіяти із зовнішнім світом. Дашборду потрібні свіжі дані з сервера. Віджет чату має слухати повідомлення. Таймер має відраховувати час. Ці операції є побічними ефектами (side effects), і вони знаходяться поза циклом рендерингу React. Хук useEffect — це місце, куди ви виносите цю хаотичну, непередбачувану роботу, щоб сам компонент залишався «чесним».
Побічні ефекти: що має бути всередині useEffect
Побічний ефект — це будь-що, що впливає на світ за межами повернення JSX. Фаза рендерингу React має бути чистою (pure). Коли ви починаєте отримувати дані, записувати значення у глобальні змінні або додавати слухачів (listeners) до DOM, ви виходите за межі «чистої» території.
Поширені приклади включають:
- Отримання даних з API
- Налаштування таймерів або інтервалів
- Додавання слухачів подій (event listeners) до
windowабоdocument - Оновлення заголовка вкладки браузера
- Підключення до WebSockets
Ці завдання мають одну спільну рису: вони не повинні бути частиною оператора return вашого компонента або основної логіки рендерингу. Спроба викликати браузерний API, наприклад setInterval, безпосередньо в тілі рендерингу призведе до його виконання під час кожного рендерингу, що створить дубльовані таймери та непередбачувану поведінку. useEffect існує саме для того, щоб ізолювати цю роботу та виконувати її в потрібний момент.
Як масив залежностей керує часом виконання
Другим аргументом useEffect є масив залежностей (dependency array), і це головне джерело плутанини для розробників, які переходять з класових компонентів. Думайте про нього як про набір змінних, за якими React стежить, щоб вирішити, чи пропустити, чи виконати ваш ефект після поточного рендерингу.
Існує три патерни, які ви використовуватимете постійно.
Повністю відсутній масив залежностей. Якщо ви взагалі не вказуєте масив, React вважає, що ви хочете, щоб ефект виконувався після кожного рендерингу, включаючи перший. Це рідко буває саме те, що вам потрібно. Якщо ваш ефект виконує мережевий запит або важку операцію з DOM, його запуск при кожному натисканні клавіші або зміні стану суттєво знизить продуктивність. Використовуйте цей патерн лише тоді, коли вам дійсно потрібно перезапускати щось, оскільки будь-який пропс або стан може змінитися, і ви не можете вказати, які саме.
Порожній масив []. Це вказує React виконати ефект один раз, одразу після монтування компонента та готовності DOM. Це правильне місце для початкового отримання даних. Наприклад, якщо ваш компонент завантажує дані профілю користувача, ви хочете, щоб цей запит відбувся рівно один раз, коли з'являється сторінка профілю, а не щоразу, коли користувач взаємодіє з формою нижче на сторінці.
Масив із конкретними змінними [count]. Це інструмент точного налаштування. React порівнює поточні значення цих залежностей із їхніми значеннями під час останнього рендерингу. Якщо будь-яка з них змінилася, ефект виконується. Якщо в списку нічого не змінилося, React повністю пропускає ефект.
Якщо ви синхронізуєте заголовок вкладки браузера зі змінною стану, ви повинні додати цю змінну в масив залежностей. Тоді React оновлюватиме заголовок лише тоді, коли це значення зміниться. Якщо ви її не додасте, заголовок залишатиметься застарілим. Якщо ж ви додасте туди непов'язані змінні стану, ви витрачатимете ресурси на оновлення заголовка через зміни, які не мають значення.
Очищення не є факультативним
Деякі ефекти залишають після себе «сліди». Таймер продовжує відлік. Слухач подій продовжує спрацьовувати. WebSocket залишається відкритим. Коли ваш компонент розмонтовується (unmounts) або навіть коли ефект перезапускається через зміну залежностей, React не очищує автоматично залишки попереднього ефекту. Це ваша робота.
Ви створюєте функцію очищення (cleanup function), повертаючи функцію зсередини useEffect. React викликає цю функцію очищення перед тим, як застосувати наступний ефект, і ще раз, коли компонент зникає з екрана.
Вам слід використовувати очищення для:
- Очищення інтервалів або тайм-аутів за допомогою
clearIntervalабоclearTimeout - Видалення слухачів подій, доданих до
window,documentабо зовнішніх вузлів (nodes) - Відписки від потоків даних або сервісів
Нехтуйте цим — і ви отримаєте витоки пам'яті (memory leaks). Компонент монтується, додає слухача прокручування, розмонтовується, а слухач залишається. Браузер утримує колбек (callback) та вузли DOM, на які він посилається. З часом, особливо в односторінкових застосунках (SPA) з інтенсивною навігацією, ці «привиди» накопичуються і сповільнюють вкладку. Виправлення зазвичай складається лише з кількох рядків: поверніть функцію, яка видаляє те, що ви додали.
Поширені помилки, які потрапляють у продакшн
Навіть досвідчені розробники звертаються до useEffect, коли існує простіший варіант. Ось три патерни, які мають викликати занепокоєння під час code review.
Нескінченні цикли. Ніколи не оновлюйте змінну стану всередині useEffect, якщо ця ж змінна входить до вашого dependency array, хіба що у вас є умова (gate condition), яка перериває цикл. Якщо ви зчитуєте count, інкрементуєте її та вказуєте count як залежність, React помічає зміну, виконує ререндер, знову запускає ефект, знову інкрементує її та блокує браузер.
Зайві ефекти. Не використовуйте useEffect для обчислення значення на основі існуючих props або state. Якщо ви можете отримати його безпосередньо під час рендерингу, просто зробіть це. Похідні значення мають бути в тілі компонента або в мемоїзованому обчисленні з useMemo. Перенесення їх в ефект розбиває вашу логіку на фази рендерингу та ефекту без жодної користі, що ускладнює читання коду.
Неправильний інструмент для дій користувача. `use
