← Назад к вопросам
Как отписаться от слушателя события?
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>
);
}
Как это работает:
- useEffect запускается после рендера
- Внутри подписываемся на событие через addEventListener
- Возвращаем функцию очистки (cleanup)
- Функция очистки вызывается перед удалением компонента или при изменении зависимостей
- В функции очистки удаляем слушателя через 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 правильной очистки
- Каждый addEventListener должен иметь removeEventListener - это базовое правило
- setInterval требует clearInterval - иначе будет выполняться бесконечно
- setTimeout требует clearTimeout - если отменить нужно было раньше выполнения
- Fetch запросы нужно отменять через AbortController - чтобы не тратить ресурсы
- Функция очистки должна быть в return useEffect - React вызовет её автоматически
Типичные ошибки
Подписка без отписки приводит к нескольким проблемам: утечка памяти (компонент удален, слушатель ещё работает), дублирование слушателей (при повторном монтировании), иногда ошибки в консоли (слушатель пытается обновить состояние удаленного компонента).