← Назад к вопросам
Что нужно добавить в useEffect в React чтобы снять обработчик после закрытия модального окна?
2.2 Middle🔥 281 комментариев
#React
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое cleanup функция?
Cleanup функция (функция очистки) в useEffect — это функция, которая выполняется когда компонент размонтируется или когда зависимости useEffect изменяются. Это критически важно для модальных окон, чтобы избежать утечек памяти и побочных эффектов.
Проблема: Утечки памяти при обработчиках событий
// Неправильно: обработчик события не удаляется
function Modal({ isOpen, onClose }) {
useEffect(() => {
const handleEscKey = (event) => {
if (event.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleEscKey);
// Слушатель остаётся! Утечка памяти!
}, [onClose]);
}
// Правильно: вернуть функцию очистки
function Modal({ isOpen, onClose }) {
useEffect(() => {
const handleEscKey = (event) => {
if (event.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleEscKey);
return () => {
window.removeEventListener("keydown", handleEscKey);
};
}, [onClose]);
}
Пример 1: Правильная работа с модальным окном
function Modal({ isOpen, onClose, title, children }) {
useEffect(() => {
if (!isOpen) return; // Не делаем ничего если modal закрыт
const handleEscapeKey = (event) => {
if (event.key === "Escape") {
onClose();
}
};
const handleClickOutside = (event) => {
const modal = document.getElementById("modal-content");
if (modal && !modal.contains(event.target)) {
onClose();
}
};
window.addEventListener("keydown", handleEscapeKey);
document.addEventListener("click", handleClickOutside);
document.body.style.overflow = "hidden";
return () => {
window.removeEventListener("keydown", handleEscapeKey);
document.removeEventListener("click", handleClickOutside);
document.body.style.overflow = "auto";
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content" id="modal-content">
<h2>{title}</h2>
<button onClick={onClose}>x</button>
<div>{children}</div>
</div>
</div>
);
}
Пример 2: Полный компонент с обработкой
function UserModal({ isOpen, user, onClose, onSave }) {
const [formData, setFormData] = useState(user);
const abortControllerRef = useRef(null);
useEffect(() => {
if (!isOpen) return;
abortControllerRef.current = new AbortController();
const handleKeyDown = (e) => {
if (e.key === "Escape") {
onClose();
}
};
const handleKeyUp = (e) => {
if (e.ctrlKey && e.key === "Enter") {
handleSave();
}
};
const handleOutsideClick = (e) => {
const content = document.getElementById("modal-form");
if (content && !content.contains(e.target)) {
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
document.addEventListener("click", handleOutsideClick);
document.body.style.overflow = "hidden";
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
document.removeEventListener("click", handleOutsideClick);
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
document.body.style.overflow = "";
};
}, [isOpen, onClose]);
const handleSave = async () => {
try {
const response = await fetch(`/api/users/${user.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
signal: abortControllerRef.current?.signal
});
if (response.ok) {
onSave(await response.json());
onClose();
}
} catch (error) {
if (error.name !== "AbortError") {
console.error("Error:", error);
}
}
};
if (!isOpen) return null;
return (
<div className="modal-overlay">
<form className="modal-form" id="modal-form">
<h2>Edit User</h2>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<div className="modal-actions">
<button type="button" onClick={onClose}>Cancel</button>
<button type="button" onClick={handleSave}>Save</button>
</div>
</form>
</div>
);
}
Пример 3: Cleanup с таймерами
// Неправильно: таймер не отменяется
function LoadingModal({ isOpen }) {
useEffect(() => {
if (!isOpen) return;
let timeout = setTimeout(() => {
console.log("Modal timeout!");
}, 5000);
// Таймер продолжит работать!
}, [isOpen]);
}
// Правильно: очистить таймер
function LoadingModal({ isOpen, autoCloseAfter = 5000 }) {
useEffect(() => {
if (!isOpen) return;
const timeout = setTimeout(() => {
console.log("Auto closing modal");
}, autoCloseAfter);
const interval = setInterval(() => {
console.log("Modal is still open");
}, 1000);
return () => {
clearTimeout(timeout);
clearInterval(interval);
};
}, [isOpen, autoCloseAfter]);
if (!isOpen) return null;
return <div className="modal">Loading...</div>;
}
Пример 4: Cleanup с WebSocket
function ModalWithWebSocket({ isOpen, onClose }) {
const wsRef = useRef(null);
useEffect(() => {
if (!isOpen) return;
wsRef.current = new WebSocket("wss://api.example.com/events");
wsRef.current.onmessage = (event) => {
console.log("Received:", event.data);
};
const handleEscape = (e) => {
if (e.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleEscape);
return () => {
if (wsRef.current) {
wsRef.current.close();
}
window.removeEventListener("keydown", handleEscape);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return <div className="modal">Connected to WebSocket</div>;
}
Ключевые правила
- Всегда возвращай функцию очистки из useEffect
- Удаляй ВСЕ добавленные слушатели
- Очищай таймеры (clearTimeout, clearInterval)
- Закрывай соединения (WebSocket.close())
- Восстанавливай измененные стили (document.body.overflow)
- Включай необходимые зависимости (isOpen, onClose)
- Используй useRef для отмены fetch запросов (AbortController)