ReactJs onClick事件导致Modal下拉菜单自动关闭问题排查
下拉菜单点击打开后立即关闭Bug修复
问题描述
- 基于React开发的操作选项下拉菜单组件,参考Udemy课程实现,此前运行正常,新增
animate-scale系列CSS类实现缩放动画效果后出现异常 - 异常表现:点击Create按钮尝试唤起菜单时,菜单会在渲染后立刻自动隐藏,无法正常停留展示
- 复现效果:

- 在线复现代码地址:
https://codesandbox.io/s/practical-jackson-6xut0y?file=/src/App.js
问题相关代码
Header组件代码(Header.js)
import React, { useEffect, useRef, useState } from "react"; import { AiOutlinePlus } from "react-icons/ai"; import { BsFillNodePlusFill, BsFillSunFill, BsNodeMinusFill, } from "react-icons/bs"; import { useTheme } from "../hooks"; export default function Header({ onAddMovieClick, onAddActorClick }) { const [showOptions, setShowOptions] = useState(false); const { toggleTheme } = useTheme(); const options = [ { title: "Add Movie", onClick: onAddMovieClick, }, { title: "Add Actor", onClick: onAddActorClick, }, ]; return ( <div className="flex justify-between items-center relative"> <input type="text" className="border-2 border-light-subtle dark:border-dark-subtle outline-none bg-transparent focus:border-primary dark:border-white transition p-1 rounded" placeholder="Search Movies..." /> <div className="flex items-center space-x-3"> <button onClick={toggleTheme} className="dark:text-white text-light-subtle p-1 rounded " > <BsFillSunFill size={24} /> </button> <button onClick={() => setShowOptions(true)} className="flex space-x-2 items-center border-secondary text-secondary border-2 font-semibold text-lg px-3 rounded" > <span>Create</span> <AiOutlinePlus /> </button> </div> <CreateOptions visible={showOptions} onClose={() => setShowOptions(false)} options={options} /> </div> ); } const CreateOptions = ({ visible, onClose, options }) => { const container = useRef(); const containerID = "option-container"; useEffect(() => { const handleClose = (e) => { if (!visible) return; const { parentElement, id } = e.target; if (parentElement.id === containerID || id === containerID) return; container.current.classList.remove("animate-scale"); container.current.classList.add("animate-scale-reverse"); }; document.addEventListener("click", handleClose); return () => { document.removeEventListener("click", handleClose); }; }, [visible]); const handleAnimationEnd = (e) => { if (e.target.classList.contains("animate-scale-reverse")) { console.log("triggered"); onClose(); } e.target.classList.remove("animate-scale"); }; if (!visible) return null; return ( <div id={containerID} ref={container} onAnimationEnd={handleAnimationEnd} className="absolute top-12 right-1 drop-shadow-lg bg-white flex flex-col p-5 space-y-3 dark:bg-secondary animate-scale" > {options.map(({ title, onClick }) => ( <Option key={title} onClick={onClick}> {title} </Option> ))} </div> ); }; const Option = ({ children, onClick }) => { return ( <button onClick={onClick} className="text-secondary dark:text-white"> {children} </button> ); };
动画样式代码(index.css)
.animate-scale { transform-origin: top; animation: scale 0.2s; } .animate-scale-reverse { transform-origin: top; animation: scale 0.2s reverse forwards; } @keyframes scale { 0% { transform: scaleY(0); } 100% { transform: scaleY(1); } }
根因分析
- 点击Create按钮触发
setShowOptions(true)时,点击事件会沿DOM树冒泡到document层,此时菜单组件刚完成挂载,useEffect中绑定的全局click监听handleClose会立刻触发 - 原有点击范围判断逻辑仅校验点击目标本身的id和直接父元素id,Create按钮不在菜单容器内部,逻辑误判为点击了菜单外部区域,直接给菜单容器添加了反向关闭动画类
animate-scale-reverse - 反向关闭动画执行完成后触发
onAnimationEnd回调,调用onClose()将showOptions设为false,菜单组件直接被卸载,最终表现为点击按钮后菜单刚出现就立刻关闭
修复方案
- 给Create按钮的点击事件添加阻止冒泡逻辑,避免点击按钮时触发document层的关闭监听:
<button onClick={(e) => { e.stopPropagation(); setShowOptions(true); }} className="flex space-x-2 items-center border-secondary text-secondary border-2 font-semibold text-lg px-3 rounded" > <span>Create</span> <AiOutlinePlus /> </button>
- 优化外部点击判断逻辑,递归向上遍历所有父节点判断点击目标是否在菜单容器内,避免菜单内嵌套多层元素时判断失效:
const handleClose = (e) => { if (!visible) return; let target = e.target; let isClickInside = false; // 向上遍历所有父节点判断是否在容器内 while (target) { if (target.id === containerID) { isClickInside = true; break; } target = target.parentElement; } if (isClickInside) return; container.current.classList.remove("animate-scale"); container.current.classList.add("animate-scale-reverse"); };
- 补全动画结束后的类名清理逻辑,移除反向动画类,避免后续打开菜单时动画异常:
const handleAnimationEnd = (e) => { if (e.target.classList.contains("animate-scale-reverse")) { onClose(); e.target.classList.remove("animate-scale-reverse"); } e.target.classList.remove("animate-scale"); };
内容的提问来源于stack exchange,提问作者Ahmad Nadeem
相关产品推荐
相关产品推荐

