You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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">
                        &times; {/* 关闭图标 */}
                    </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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 01:02:03