React中如何判断Dialog组件是否处于顶层并实现Esc键逐层关闭?
React Dialog 按Esc逐层关闭的优化实现
你遇到的核心问题是多个Dialog同时监听Esc事件,导致按下一次会触发所有关闭逻辑。要实现逐层关闭,关键是让只有当前顶层的Dialog响应Esc按键,下面是两种适合React新手的可行方案:
方案一:用Context管理Dialog栈(推荐)
通过全局Context维护一个Dialog关闭方法的栈,只有栈顶的Dialog会执行关闭操作,完全避免props drilling的麻烦:
1. 创建Dialog上下文 Provider
import { createContext, useContext, useEffect, useState } from 'react'; const DialogContext = createContext(); export function DialogProvider({ children }) { const [dialogStack, setDialogStack] = useState([]); // 将Dialog的关闭方法加入栈顶 const pushDialog = (closeFn) => { setDialogStack(prev => [...prev, closeFn]); }; // 卸载时从栈中移除对应方法 const popDialog = (closeFn) => { setDialogStack(prev => prev.filter(fn => fn !== closeFn)); }; // 全局监听Esc键,只触发栈顶的关闭方法 useEffect(() => { const handleEsc = (e) => { if (e.key === 'Escape' && dialogStack.length > 0) { dialogStack[dialogStack.length - 1](); } }; window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); }, [dialogStack]); return ( <DialogContext.Provider value={{ pushDialog, popDialog }}> {children} </DialogContext.Provider> ); }
2. 改造Dialog组件
import { useContext, useEffect, useRef } from 'react'; import { DialogContext } from './DialogProvider'; export function Dialog({ isOpen, onClose, children }) { const { pushDialog, popDialog } = useContext(DialogContext); const closeRef = useRef(onClose); // 实时更新关闭方法引用 useEffect(() => { closeRef.current = onClose; }, [onClose]); // 打开时入栈,关闭时出栈 useEffect(() => { if (isOpen) { const closeFn = () => closeRef.current(); pushDialog(closeFn); return () => popDialog(closeFn); } }, [isOpen, pushDialog, popDialog]); if (!isOpen) return null; return ( <div className="dialog-overlay"> <div className="dialog-content"> {children} </div> </div> ); }
3. 全局挂载Provider
在你的根组件(比如App.js)中包裹:
function App() { return ( <DialogProvider> {/* 你的应用内容 */} </DialogProvider> ); }
这个方案的优势是逻辑清晰,完全不依赖DOM结构,不管组件嵌套多复杂都能正常工作。
方案二:基于DOM层级判断顶层Dialog
如果不想用Context,可以通过判断Dialog的z-index来确定顶层,只有顶层Dialog注册Esc事件:
自定义Hook实现
import { useEffect } from 'react'; export function useTopDialog(isOpen, onClose, dialogId) { useEffect(() => { if (!isOpen) return; const handleEsc = (e) => { if (e.key !== 'Escape') return; // 获取所有已打开的Dialog元素 const allOpenDialogs = Array.from(document.querySelectorAll('.dialog-overlay:visible')); if (allOpenDialogs.length === 0) return; // 找出z-index最高的Dialog const topDialog = allOpenDialogs.reduce((prev, curr) => { const prevZ = parseInt(window.getComputedStyle(prev).zIndex, 10) || 0; const currZ = parseInt(window.getComputedStyle(curr).zIndex, 10) || 0; return currZ > prevZ ? curr : prev; }); // 当前Dialog是顶层时才关闭 if (topDialog.id === dialogId) { onClose(); } }; window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); }, [isOpen, onClose, dialogId]); }
在Dialog中使用Hook
export function Dialog({ isOpen, onClose, id, children }) { useTopDialog(isOpen, onClose, id); if (!isOpen) return null; return ( <div id={id} className="dialog-overlay" style={{ display: isOpen ? 'block' : 'none' }}> <div className="dialog-content"> {children} </div> </div> ); }
注意:使用这个方案需要确保每个Dialog的z-index是递增的(比如打开新Dialog时设置更高的z-index),否则判断会出错。
内容的提问来源于stack exchange,提问作者Tuan
相关产品推荐
相关产品推荐

