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

Что нужно добавить в 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>;
}

Ключевые правила

  1. Всегда возвращай функцию очистки из useEffect
  2. Удаляй ВСЕ добавленные слушатели
  3. Очищай таймеры (clearTimeout, clearInterval)
  4. Закрывай соединения (WebSocket.close())
  5. Восстанавливай измененные стили (document.body.overflow)
  6. Включай необходимые зависимости (isOpen, onClose)
  7. Используй useRef для отмены fetch запросов (AbortController)