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

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

2.0 Middle🔥 121 комментариев
#Soft Skills и рабочие процессы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Управление жизненным циклом event listeners

Слушатели событий — это память. Если их не удалять, возникают утечки памяти, падает производительность и могут появляться непредсказуемые баги. Правило простое: если подписался на событие — обязательно отпишись, когда это больше не нужно. Это касается DOM-событий, таймеров, сетевых запросов и других асинхронных операций.

Правильный способ с React Hooks (useEffect)

import { useEffect, useState } from 'react';

export function WindowSizeMonitor() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    // Функция слушателя
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    // Подписываемся на событие resize
    window.addEventListener('resize', handleResize);

    // ВАЖНО: функция очистки (cleanup function)
    // Вызывается перед удалением компонента или перед повторным запуском эффекта
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Пустой массив зависимостей = эффект выполнится только один раз

  return (
    <div>
      <p>Ширина: {windowSize.width}px</p>
      <p>Высота: {windowSize.height}px</p>
    </div>
  );
}

Как это работает:

  1. useEffect запускается после рендера
  2. Внутри подписываемся на событие через addEventListener
  3. Возвращаем функцию очистки (cleanup)
  4. Функция очистки вызывается перед удалением компонента или при изменении зависимостей
  5. В функции очистки удаляем слушателя через removeEventListener

Работа с таймерами и интервалами

import { useEffect, useState } from 'react';

export function StopWatch() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  useEffect(() => {
    if (!isRunning) return; // Если не запущен, ничего не делаем

    // setInterval возвращает ID таймера
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    // Функция очистки удаляет интервал
    return () => clearInterval(intervalId);
  }, [isRunning]); // Зависит от isRunning

  return (
    <div>
      <p>Секунд: {seconds}</p>
      <button onClick={() => setIsRunning(!isRunning)}>
        {isRunning ? 'Остановить' : 'Начать'}
      </button>
    </div>
  );
}

Подписка на сетевые события (AbortController)

import { useEffect, useState } from 'react';

interface User {
  id: number;
  name: string;
}

export function UserFetcher() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    setLoading(true);
    setError(null);

    // AbortController позволяет отменить fetch запрос
    const abortController = new AbortController();

    const fetchUser = async () => {
      try {
        const response = await fetch('/api/v1/user/profile', {
          signal: abortController.signal, // Передаём signal
        });

        if (!response.ok) {
          throw new Error('Ошибка загрузки');
        }

        const data = await response.json();
        setUser(data);
      } catch (err) {
        // AbortError возникает, когда запрос отменили
        if (err instanceof Error && err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchUser();

    // Функция очистки отменяет запрос
    return () => abortController.abort();
  }, []);

  if (loading) return <p>Загрузка...</p>;
  if (error) return <p>Ошибка: {error}</p>;
  return <p>Пользователь: {user?.name}</p>;
}

Работа с кастомными слушателями (EventEmitter)

import { useEffect, useState } from 'react';

class SimpleEventEmitter {
  private listeners: Map<string, Function[]> = new Map();

  on(event: string, callback: Function) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)?.push(callback);
  }

  off(event: string, callback: Function) {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    }
  }

  emit(event: string, data: any) {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      callbacks.forEach(cb => cb(data));
    }
  }
}

const eventBus = new SimpleEventEmitter();

export function NotificationListener() {
  const [notifications, setNotifications] = useState<string[]>([]);

  useEffect(() => {
    const handleNotification = (message: string) => {
      setNotifications(prev => [...prev, message]);
    };

    // Подписываемся на событие
    eventBus.on('notification', handleNotification);

    // Отписываемся в функции очистки
    return () => {
      eventBus.off('notification', handleNotification);
    };
  }, []);

  return (
    <ul>
      {notifications.map((notif, i) => (
        <li key={i}>{notif}</li>
      ))}
    </ul>
  );
}

Checklist правильной очистки

  1. Каждый addEventListener должен иметь removeEventListener - это базовое правило
  2. setInterval требует clearInterval - иначе будет выполняться бесконечно
  3. setTimeout требует clearTimeout - если отменить нужно было раньше выполнения
  4. Fetch запросы нужно отменять через AbortController - чтобы не тратить ресурсы
  5. Функция очистки должна быть в return useEffect - React вызовет её автоматически

Типичные ошибки

Подписка без отписки приводит к нескольким проблемам: утечка памяти (компонент удален, слушатель ещё работает), дублирование слушателей (при повторном монтировании), иногда ошибки в консоли (слушатель пытается обновить состояние удаленного компонента).