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

React模态窗口无法关闭:成因及非setTimeout解决方案咨询

React模态框无法关闭的问题分析与解决

问题重现

以下是你的TransactionItem组件代码:

import c from '../Profile.module.css'; 
import { useState } from 'react';
import { createPortal } from 'react-dom';

import Modal from '../Modal/Modal';

const TransactionItem = (props) => {
    const modalRoot = document.querySelector('#root > div');
    const [showModal, setShowModal] = useState(false);
    const short = str => str.substring(str.length - 4) + '...' + str.substring(0, 5);
    
    const handleClose = () => {
        setShowModal(false);
    }

    return (
        <div className={c.transaction_item} onClick={() => setShowModal(true)}>
            <div className={c.transaсtion_data}>
                <div className={c.icon}><span>$</span></div>
                <div className={c.transaction_info}>
                    {props.type === "+" ? 'Пополнение' : 'Перевод'}
                    <div className={c.transaction_where}>{short(props.from)} -> {short(props.to)}</div>
                </div>
            </div>
            <span className={c.transaction_total}>{props.type + props.total}</span>
            {showModal && createPortal(
                <Modal onClose={handleClose} date='23.05.2024 в 09:41' {...props} />,
                modalRoot
            )}
        </div>
    )
} 

export default TransactionItem;

点击打开模态窗口后无法关闭,只有将handleClose修改为以下代码才能正常关闭:

const handleClose = () => {
    setTimeout(() => {
        setShowModal(false);
    }, 0)
}

问题成因

核心原因是事件冒泡:
虽然你用createPortal把Modal渲染到了DOM的其他位置,但React的事件系统是基于组件树而非真实DOM树工作的。当点击Modal的关闭按钮时,事件会沿着React组件树向上冒泡,触发TransactionItem外层div的onClick={() => setShowModal(true)}。
这就导致一个矛盾:你刚调用setShowModal(false)关闭模态框,冒泡上来的事件又立刻把showModal设回true,视觉上就表现为模态框关不掉。

而setTimeout能生效,是因为它把状态更新放到了事件循环的下一轮执行,此时父元素的onClick事件已经执行完毕,不会再覆盖你的状态更新。

替代解决方案(无需setTimeout)

方案一:阻止事件冒泡

在Modal组件内部,给关闭按钮或Modal的外层容器添加事件处理,阻止事件向上冒泡:
比如在Modal的关闭按钮点击时:

// Modal组件内部的关闭按钮逻辑
const handleClose = (e) => {
  e.stopPropagation(); // 阻止事件冒泡到父组件
  props.onClose();
};

或者直接给Modal的最外层容器添加onClick阻止冒泡:

// Modal组件的根元素
<div className="modal" onClick={(e) => e.stopPropagation()}>
  {/* Modal内容 */}
  <button onClick={props.onClose}>关闭</button>
</div>

方案二:拆分触发区域

把打开模态框的点击事件,从整个transaction_item容器移到专门的触发元素上,比如只让交易信息区域触发打开操作:

// 修改TransactionItem的返回结构
return (
  <div className={c.transaction_item}>
    {/* 仅该区域触发模态框打开 */}
    <div className={c.transaсtion_data} onClick={() => setShowModal(true)}>
      <div className={c.icon}><span>$</span></div>
      <div className={c.transaction_info}>
        {props.type === "+" ? 'Пополнение' : 'Перевод'}
        <div className={c.transaction_where}>{short(props.from)} -> {short(props.to)}</div>
      </div>
    </div>
    <span className={c.transaction_total}>{props.type + props.total}</span>
    {showModal && createPortal(
      <Modal onClose={handleClose} date='23.05.2024 в 09:41' {...props} />,
      modalRoot
    )}
  </div>
);

这样点击Modal时,不会触发父元素的打开逻辑,自然不会出现状态被覆盖的问题。

方案三:父元素点击事件过滤

在父元素的onClick中,判断点击目标是否属于Modal相关元素,若是则不执行打开逻辑:

// TransactionItem中的点击处理
const handleOpen = (e) => {
  // 判断点击目标是否是Modal或其后代元素
  if (e.target.closest('.modal')) return;
  setShowModal(true);
};

// 外层div的onClick改为handleOpen
<div className={c.transaction_item} onClick={handleOpen}>

这种方法需要给Modal添加统一的类名,不如前两种方案直接简洁。

内容的提问来源于stack exchange,提问作者Lorenzo Lavantelli

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 13:43:23