← Назад к вопросам

Как отписаться от слушателя события в 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>;
}

Чек-лист для правильной очистки

  1. addEventListener -> removeEventListener (парные вызовы)
  2. setInterval -> clearInterval
  3. setTimeout -> clearTimeout
  4. CustomEvent subscriptions -> unsubscribe
  5. AbortController -> controller.abort()
  6. Правильно указывай зависимости в массиве []
  7. Функция очистки вызывается ДО монтирования нового эффекта

Частые ошибки

// ❌ Ошибка 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".

Как отписаться от слушателя события в useEffect? | PrepBro