← Назад к вопросам
Как отписаться от слушателя события в useEffect?
1.7 Middle🔥 231 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как отписаться от слушателя события в useEffect
Очистка (cleanup) в useEffect критична для предотвращения утечек памяти. Когда ты добавляешь слушатель события, нужно удалить его когда компонент размонтируется или зависимость изменится. Утечка памяти происходит если слушателей остаётся в памяти больше чем надо.
Базовый пример
// ❌ Плохо - утечка памяти, слушатель никогда не удаляется
useEffect(() => {
window.addEventListener("scroll", handleScroll);
}, []);
// ✅ Хорошо - удаляем слушатель в функции очистки
useEffect(() => {
const handleScroll = () => {
console.log("Scrolled:", window.scrollY);
};
window.addEventListener("scroll", handleScroll);
// Функция очистки - вызывается перед размонтированием компонента
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
Почему это критично?
// Представь компонент, монтирующийся/демонтирующийся несколько раз
// ❌ Без очистки - утечка памяти
export function Page() {
useEffect(() => {
// Каждый раз добавляется НОВЫЙ слушатель в памяти
window.addEventListener("resize", handleResize);
// После 10 монтирований = 10 слушателей в памяти одновременно!
}, []);
return <div>Page</div>;
}
// ✅ С очисткой - память освобождается
export function Page() {
useEffect(() => {
window.addEventListener("resize", handleResize);
// При размонтировании слушатель удаляется из памяти
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <div>Page</div>;
}
Практический пример: отзывчивая ширина
import { useEffect, useState } from "react";
function ResponsiveComponent() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
// Функция-обработчик
const handleResize = () => {
setWidth(window.innerWidth);
};
// Добавляем слушатель
window.addEventListener("resize", handleResize);
// Функция очистки - удаляем слушатель
return () => {
window.removeEventListener("resize", handleResize);
};
}, []); // пустой массив = только при монтировании/размонтировании
return <div>Width: {width}px</div>;
}
Несколько слушателей
useEffect(() => {
const handleScroll = () => console.log("scroll");
const handleResize = () => console.log("resize");
const handleClick = () => console.log("click");
// Добавляем всех слушателей
window.addEventListener("scroll", handleScroll);
window.addEventListener("resize", handleResize);
document.addEventListener("click", handleClick);
// Очищаем ВСЕ слушателей в одной функции
return () => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleResize);
document.removeEventListener("click", handleClick);
};
}, []);
С зависимостями в массиве
function Dashboard({ userId }) {
useEffect(() => {
const handleUserUpdate = (event) => {
console.log("User updated:", event.detail);
};
// Слушаем кастомное событие
document.addEventListener("userUpdate", handleUserUpdate);
// Очищаем при изменении userId или размонтировании
return () => {
document.removeEventListener("userUpdate", handleUserUpdate);
};
}, [userId]); // Когда userId меняется, старый слушатель удаляется, создаётся новый
}
AbortController - современный подход
// Более элегантный и современный способ с AbortController (ES2017+)
useEffect(() => {
const controller = new AbortController();
const handleScroll = () => {
console.log("Scrolled");
};
// Передаём signal для управления жизненным циклом
window.addEventListener("scroll", handleScroll, {
signal: controller.signal
});
// Очистка - отменяем все слушатели этого контроллера одним вызовом
return () => {
controller.abort();
};
}, []);
Таймеры и интервалы
// setTimeout - очистка
useEffect(() => {
const timeoutId = setTimeout(() => {
console.log("Timer finished");
}, 1000);
return () => clearTimeout(timeoutId);
}, []);
// setInterval - очистка
useEffect(() => {
const intervalId = setInterval(() => {
console.log("Tick");
}, 1000);
return () => clearInterval(intervalId);
}, []);
Полный пример: бесконечный скролл
function InfiniteScroll({ onLoadMore }) {
const [page, setPage] = useState(1);
useEffect(() => {
const handleScroll = () => {
// Проверяем достигли ли конца страницы
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) {
onLoadMore();
setPage(prev => prev + 1);
}
};
// Добавляем слушатель
window.addEventListener("scroll", handleScroll);
// Очищаем слушатель при размонтировании или изменении зависимостей
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [onLoadMore]); // Зависимость от onLoadMore чтобы использовать актуальную версию
return <div className="loading">Loading more items...</div>;
}
Чек-лист для правильной очистки
addEventListener->removeEventListener(парные вызовы)setInterval->clearIntervalsetTimeout->clearTimeout- CustomEvent subscriptions -> unsubscribe
- AbortController ->
controller.abort() - Правильно указывай зависимости в массиве
[] - Функция очистки вызывается ДО монтирования нового эффекта
Частые ошибки
// ❌ Ошибка 1 - забыли return
useEffect(() => {
window.addEventListener("scroll", handler);
// Нет очистки! Утечка памяти
});
// ❌ Ошибка 2 - неправильный обработчик
useEffect(() => {
const handler = () => {};
window.addEventListener("scroll", handler);
return () => {
// ❌ Другая функция!
window.removeEventListener("scroll", () => {});
};
});
// ✅ Правильно - одна и та же функция
useEffect(() => {
const handler = () => {};
window.addEventListener("scroll", handler);
return () => {
window.removeEventListener("scroll", handler);
};
});
Итог: Функция очистки в useEffect обязательна для удаления слушателей, таймеров, подписок. Без неё произойдут утечки памяти и могут возникнуть ошибки типа "Cannot perform a React state update on an unmounted component".