Как в useEffect в React снять обработчик при размонтировании компонента?
Хуки
Обработчик снимается в cleanup-функции эффекта: то, что эффект повесил, возвращаемая функция снимает — React вызовет её при размонтировании.
Базовый паттерн
useEffect(() => {
const handleScroll = () => setOffset(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll); // при размонтировании
};
}, []);Критично: removeEventListener должен получить ту же самую ссылку на функцию, что и addEventListener, — поэтому обработчик объявляют внутри эффекта (общее замыкание), а не инлайном в обоих вызовах.
Нюансы
- С пустым массивом deps cleanup выполнится только при размонтировании; с
[deps]— ещё и перед каждым перезапуском эффекта (старый обработчик снимется, новый повесится — это правильно). - StrictMode (dev) монтирует компонент дважды: подписка → отписка → подписка. Если отписка написана честно, дублей не будет — так React её и проверяет.
- Обработчик, использующий state, лучше держать «стабильным»: функциональные сеттеры (
setX((x) => ...)) избавляют от пересоздания подписки на каждое изменение.
Современная альтернатива: AbortController
Один signal снимает пачку слушателей:
useEffect(() => {
const ctrl = new AbortController();
window.addEventListener('scroll', onScroll, { signal: ctrl.signal });
window.addEventListener('resize', onResize, { signal: ctrl.signal });
return () => ctrl.abort(); // снимает оба
}, []);Коротко для собеседования
Повесить слушатель в эффекте, вернуть cleanup с removeEventListener той же функции (объявленной внутри эффекта); с [] cleanup сработает при размонтировании, с deps — и перед перезапусками. Проверяется StrictMode-двойным монтированием; для нескольких слушателей удобен AbortController с signal.