React严格模式下useEffect清理函数未修复模态框无法打开问题
问题
开发支持点击关闭按钮或外部区域关闭的模态框组件时,为实现外部点击关闭功能,自定义了useDidMountEffect Hook让useEffect回调仅在isModalPopedUp状态变化时执行、跳过初始渲染。但在React严格模式下,组件被重复挂载后,useDidMountEffect的回调提前给document添加了点击事件监听,此时isModalPopedUp还未被打开按钮设为true。点击打开按钮时,isModalPopedUp被设为true后立刻被提前添加的事件监听改为false,导致模态框无法打开。已添加清理函数但问题仍未解决。
相关代码
自定义Hook useDidMountEffect
import { useRef, useEffect } from "react"; // 控制useEffect回调是否在初始渲染时执行的自定义Hook export default function useDidMountEffect(argObject = {}) { /* argObject = { runOnInitialRender: boolean, callback: () => void, dependencies: any[] // useEffect的依赖数组 } */ const didMountRef = useRef(argObject.runOnInitialRender); useEffect(() => { if (didMountRef.current) { argObject.callback(); } else { didMountRef.current = true; } }, argObject.dependencies); }
父组件 ModalParent
import { useEffect, useState } from "react"; import Modal from "./modal"; import useDidMountEffect from "./useDidMountHook"; import "./modal.css"; export default function ModalParent() { const [isModalPopedUp, setIsModalPopedUp] = useState(false); function handleToggleModalPopup() { setIsModalPopedUp((prevState) => !prevState); } function handleOnClose() { setIsModalPopedUp(false); } function handleOutsideModalClick(event) { !event.target.classList.contains("modal-content") && handleOnClose(); } useDidMountEffect({ runOnInitialRender: false, callback: () => { if (isModalPopedUp) { document.addEventListener("click", handleOutsideModalClick); } else { document.removeEventListener("click", handleOutsideModalClick); } return () => { document.removeEventListener("click", handleOutsideModalClick); }; }, dependencies: [isModalPopedUp], }); return ( <div> <button onClick={handleToggleModalPopup}> Open Modal Popup </button> {isModalPopedUp && ( <Modal header={<h1>Customised Header</h1>} footer={<h1>Customised Footer</h1>} onClose={handleOnClose} body={<div>Customised Body</div>} /> )} </div> ); }
模态框组件 Modal
export default function Modal({ id, header, body, footer, onClose }) { return ( <div id={id || "modal"} className="modal"> <div className="modal-content"> <div className="header"> <span onClick={onClose} className="close-modal-icon"> × {/* 关闭图标 */} </span> <div>{header ? header : "Header"}</div> </div> <div className="body"> {body ? ( body ) : ( <div> <p>This is our Modal body</p> </div> )} </div> <div className="footer"> {footer ? footer : <h2>footer</h2>} </div> </div> </div> ); }
样式文件 modal.css
/** @format */ .modal { position: fixed; z-index: 1; padding-top: 2rem; left: 0; top: 0; width: 100%; height: 100%; overflow: hidden; background-color: #b19b9b; color: black; overflow: auto; padding-bottom: 2rem; } .modal-content { position: relative; background-color: #fefefe; margin: auto; padding: 0; border: 1px solid red; width: 80%; animation-name: animateModal; animation-duration: 0.5s; animation-timing-function: ease-in-out; } .close-modal-icon { cursor: pointer; font-size: 40px; position: absolute; top: 0.5rem; right: 0.5rem; font-weight: bold; } .header { padding: 4px 10px; background-color: #5cb85c; color: white; } .body { padding: 2px 16px; height: 200px; } .footer { padding: 4px 16px; background-color: #5cb85c; color: white; } @keyframes animateModal { from { top: -200px; opacity: 0; } to { top: 0; opacity: 1; } }
修复方案
1. 核心问题分析
问题本质是两个原因叠加:
- 点击打开按钮的事件会冒泡到document,触发已添加的监听,直接关闭刚打开的模态框
- 严格模式下组件重复挂载,加上函数引用不稳定,导致监听清理不彻底,积累无效监听
2. 分步修复
(1)稳定事件监听函数引用
使用useCallback包裹handleOutsideModalClick,避免每次渲染创建新函数,确保removeEventListener能正确移除旧监听:
import { useCallback } from "react"; // ... const handleOutsideModalClick = useCallback((event) => { const modalContent = document.querySelector(".modal-content"); // 优化判断逻辑:点击区域不在模态框内容内部才关闭 if (modalContent && !modalContent.contains(event.target)) { handleOnClose(); } }, []);
(2)替换自定义Hook为标准useEffect
移除useDidMountEffect,直接用标准useEffect控制监听的添加/移除,避免自定义Hook在严格模式下的逻辑漏洞:
useEffect(() => { // 初始状态isModalPopedUp为false,直接跳过 if (!isModalPopedUp) return; document.addEventListener("click", handleOutsideModalClick); // 清理函数:组件卸载或isModalPopedUp变为false时执行 return () => { document.removeEventListener("click", handleOutsideModalClick); }; }, [isModalPopedUp, handleOutsideModalClick]);
(3)阻止打开按钮的事件冒泡
点击打开按钮时,阻止事件冒泡到document,避免触发外部点击监听:
<button onClick={(e) => { e.stopPropagation(); handleToggleModalPopup(); }}> Open Modal Popup </button>
3. 修复后完整的ModalParent代码
import { useCallback, useState, useEffect } from "react"; import Modal from "./modal"; import "./modal.css"; export default function ModalParent() { const [isModalPopedUp, setIsModalPopedUp] = useState(false); function handleToggleModalPopup() { setIsModalPopedUp((prevState) => !prevState); } function handleOnClose() { setIsModalPopedUp(false); } const handleOutsideModalClick = useCallback((event) => { const modalContent = document.querySelector(".modal-content"); if (modalContent && !modalContent.contains(event.target)) { handleOnClose(); } }, []); useEffect(() => { if (!isModalPopedUp) return; document.addEventListener("click", handleOutsideModalClick); return () => { document.removeEventListener("click", handleOutsideModalClick); }; }, [isModalPopedUp, handleOutsideModalClick]); return ( <div> <button onClick={(e) => { e.stopPropagation(); handleToggleModalPopup(); }}> Open Modal Popup </button> {isModalPopedUp && ( <Modal header={<h1>Customised Header</h1>} footer={<h1>Customised Footer</h1>} onClose={handleOnClose} body={<div>Customised Body</div>} /> )} </div> ); }
内容的提问来源于stack exchange,提问作者onyedikachi23
相关产品推荐
相关产品推荐

