Пошук уроків, статей та іншого контенту
Навчитеся виконувати побічні ефекти через useEffect, керувати залежностями та очищати ресурси під час розмонтування.
Рендер React-компонента має бути передбачуваним: за однакових props і state він має повертати однаковий JSX. Під час рендеру не варто:
змінювати заголовок документа;
запускати таймери;
додавати обробники подій до window або document;
виконувати HTTP-запити;
підписуватися на зовнішні джерела даних;
змінювати DOM напряму.
Такі дії називають побічними ефектами. Для них React надає хук useEffect.
import { useEffect } from "react";
useEffect(() => {
// Побічний ефект
});Функція всередині useEffect виконується після того, як React оновив DOM.
useEffectuseEffect приймає два аргументи:
функцію ефекту;
масив залежностей.
useEffect(() => {
// Код ефекту
return () => {
// Очищення ефекту
};
}, [dependency1, dependency2]);Функція очищення є необов’язковою. Вона потрібна, коли ефект створює ресурс, який необхідно звільнити:
таймер;
обробник події;
підписку;
незавершений запит;
з’єднання із зовнішнім сервісом.
useEffect(() => {
console.log("Ефект виконується після кожного рендеру");
});Такий ефект виконується після кожного рендеру компонента. Це рідко потрібно, оскільки необмежене виконання може створити зайві операції або нескінченний цикл оновлень.
useEffect(() => {
console.log("Ефект виконується після монтування");
}, []);Ефект виконується після першого монтування компонента. Функція очищення виконується під час розмонтування.
useEffect(() => {
const timerId = setTimeout(() => {
console.log("Минув час");
}, 1000);
return () => {
clearTimeout(timerId);
};
}, []);У режимі розробки StrictMode React може додатково виконати послідовність «ефект — очищення — ефект», щоб виявити небезпечні побічні ефекти. Тому ефекти та їх очищення мають бути безпечними для повторного виконання.
useEffect(() => {
console.log("Поточний ідентифікатор:", userId);
}, [userId]);Ефект запускається:
після першого монтування;
щоразу, коли змінюється userId.
React порівнює попередні та нові залежності за допомогою порівняння, подібного до Object.is.
Функція ефекту може повернути функцію очищення:
useEffect(() => {
const timerId = setInterval(() => {
console.log("Тік");
}, 1000);
return () => {
clearInterval(timerId);
};
}, []);Очищення відбувається у двох випадках:
перед повторним запуском ефекту через зміну залежностей;
під час розмонтування компонента.
Наприклад, якщо ефект залежить від roomId, підписка на стару кімнату має бути скасована перед підпискою на нову:
useEffect(() => {
const connection = connectToRoom(roomId);
return () => {
connection.disconnect();
};
}, [roomId]);Так компонент не залишає активними старі підписки.
У цьому прикладі:
кнопка запускає та зупиняє таймер;
setInterval створюється лише під час роботи таймера;
інтервал очищається під час зупинки або розмонтування;
заголовок документа синхронізується зі станом;
функціональне оновлення стану запобігає використанню застарілого значення.
import { StrictMode, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
function Timer() {
const [isRunning, setIsRunning] = useState(false);
const [seconds, setSeconds] = useState(0);
useEffect(() => {
if (!isRunning) {
return;
}
const intervalId = setInterval(() => {
// Беремо актуальне значення стану
setSeconds((currentSeconds) => currentSeconds + 1);
}, 1000);
return () => {
// Зупиняємо інтервал перед повторним запуском або розмонтуванням
clearInterval(intervalId);
};
}, [isRunning]);
useEffect(() => {
document.title = `Час: ${seconds} с`;
return () => {
// Відновлюємо заголовок після розмонтування компонента
document.title = "React";
};
}, [seconds]);
function handleReset() {
setSeconds(0);
}
return (
<main>
<h1>Таймер: {seconds} с</h1>
<button onClick={() => setIsRunning((running) => !running)}>
{isRunning ? "Зупинити" : "Запустити"}
</button>
<button onClick={handleReset}>Скинути</button>
</main>
);
}
function App() {
return <Timer />;
}
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>,
);Залежність першого ефекту — isRunning, а не seconds. Коли змінюється кількість секунд, інтервал не потрібно створювати заново. Інтервал має працювати доти, доки isRunning не зміниться на false.
У виклику setSeconds використано функціональну форму:
setSeconds((currentSeconds) => currentSeconds + 1);Вона отримує найсвіжіше значення стану. Це особливо важливо в асинхронних колбеках, таких як таймери.
До залежностей потрібно додавати значення, які:
оголошені в тілі компонента;
використовуються всередині ефекту;
можуть змінюватися між рендерами.
function UserGreeting({ user }) {
useEffect(() => {
console.log(`Користувач: ${user.name}`);
}, [user]);
return <p>{user.name}</p>;
}Якщо ефект використовує конкретне поле, часто краще залежати від нього, а не від усього об’єкта:
function UserGreeting({ user }) {
useEffect(() => {
console.log(`Користувач: ${user.name}`);
}, [user.name]);
return <p>{user.name}</p>;
}Об’єкти та функції порівнюються за посиланням. Тому навіть однакові за вмістом об’єкти можуть вважатися різними:
const firstUser = { name: "Олена" };
const secondUser = { name: "Олена" };
console.log(firstUser === secondUser); // falseЯкщо об’єкт створюється під час кожного рендеру та використовується як залежність, ефект може запускатися після кожного рендеру. У такій ситуації варто:
використовувати конкретні примітивні поля як залежності;
створювати об’єкт усередині ефекту, якщо він потрібен лише там;
не додавати залежності механічно без розуміння їхнього значення.
Ефект запам’ятовує значення, доступні під час конкретного рендеру. Якщо залежність пропущена, ефект може працювати зі старим значенням.
Помилковий приклад:
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);Тут seconds не вказано в залежностях. Колбек таймера може постійно використовувати значення seconds, яке було під час першого рендеру.
Є два правильні підходи:
додати значення до залежностей, якщо ефект справді має перезапускатися при його зміні;
використати функціональне оновлення стану, якщо потрібне лише попереднє значення.
Для таймера другий підхід кращий:
setSeconds((currentSeconds) => currentSeconds + 1);Ефект призначений для синхронізації компонента із зовнішньою системою після рендеру. Обробники подій призначені для реакції на конкретну дію користувача.
Не обов’язково використовувати ефект для простої зміни стану після натискання кнопки:
function Form() {
const [submitted, setSubmitted] = useState(false);
function handleSubmit(event) {
event.preventDefault();
// Дію, пов'язану з натисканням кнопки, виконуємо в обробнику
setSubmitted(true);
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Надіслати</button>
{submitted && <p>Форму надіслано</p>}
</form>
);
}Використовуйте useEffect, коли потрібно реагувати на зміну стану або властивостей і синхронізувати їх із зовнішнім ресурсом.
Якщо не очистити ресурс, кожен запуск ефекту може створити додатковий таймер або обробник.
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);Для видалення обробника потрібно передати ту саму функцію, яку було використано під час додавання.
useEffect(() => {
console.log(query);
}, []);Якщо query може змінюватися, його потрібно додати до залежностей:
useEffect(() => {
console.log(query);
}, [query]);Правило ESLint react-hooks/exhaustive-deps допомагає знаходити такі помилки. Не варто вимикати це правило, не розібравшись із причиною попередження.
useEffect(() => {
setCount(count + 1);
}, [count]);Кожне оновлення count повторно запускає ефект, який знову змінює count. Це створює нескінченний цикл.
Перед додаванням setState до ефекту перевірте, чи справді оновлення має відбуватися після кожної зміни залежностей.
Не передавайте async безпосередньо в useEffect:
// Неправильно
useEffect(async () => {
// ...
}, []);useEffect очікує, що колбек поверне функцію очищення або нічого. async-функція завжди повертає Promise.
Асинхронну логіку можна оголосити всередині ефекту:
useEffect(() => {
let isActive = true;
async function loadData() {
const response = await fetch("/api/data");
const data = await response.json();
if (isActive) {
console.log(data);
}
}
loadData();
return () => {
// Не даємо застарілому результату змінити стан після очищення
isActive = false;
};
}, []);useEffect виконує побічні ефекти після оновлення компонента.
Масив залежностей визначає, коли ефект має запускатися повторно.
Без масиву ефект запускається після кожного рендеру.
Порожній масив означає запуск після монтування та очищення під час розмонтування.
Очищення повертається з функції ефекту та виконується перед повторним запуском або розмонтуванням.
До масиву залежностей потрібно додавати всі змінні, від яких залежить поведінка ефекту.
Для таймерів, підписок і обробників подій завжди потрібно передбачати очищення.
Для оновлення стану в асинхронних колбеках використовуйте функціональну форму setState.
useEffect не замінює обробники подій і не потрібен для обчислення значень, які можна отримати під час рендеру.