关闭MUI Dialog时触发Trap Focus栈溢出错误求助
问题描述
开发了一个应用启动时自动弹出的MUI Dialog,每次尝试关闭都会出现栈溢出错误:
Uncaught RangeError: Maximum call stack size exceeded. at HTMLDocument.contain (FocusTrap.js:225:1) at HTMLDocument.contain (FocusTrap.js:225:1) at HTMLDocument.contain (FocusTrap.js:225:1) ...(重复报错)
已尝试使用MUI官方示例代码,但在当前代码库中仍触发错误,官方环境则正常。当前仅创建了一个Dialog,不符合常见的多Dialog触发错误的情况。
当前行为:关闭Dialog需多次点击,控制台大量Trap Focus错误。
预期行为:点击一次即可关闭Dialog,无Trap Focus错误。
上下文:希望应用启动时弹出Dialog展示信息,当前问题已影响用户体验。
代码实现
import React, { useCallback, useEffect, useRef, useState } from 'react'; import { compose, withProps } from 'recompose'; import { withErrorBoundary } from 'react-error-boundary'; import { Redirect, Route, Switch } from 'react-router-dom'; import { PageNotFoundView } from './PageNotFoundView'; import SplashPage from './SplashPage'; import LoadingPanel from './LoadingPanel'; import Dialog from '@mui/material/Dialog'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import Button from '@mui/material/Button'; import PropTypes from 'prop-types'; import Avatar from '@mui/material/Avatar'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemAvatar from '@mui/material/ListItemAvatar'; import ListItemButton from '@mui/material/ListItemButton'; import ListItemText from '@mui/material/ListItemText'; import PersonIcon from '@mui/icons-material/Person'; import AddIcon from '@mui/icons-material/Add'; import Typography from '@mui/material/Typography'; import { blue } from '@mui/material/colors'; let Apps = []; if (process.env.NODE_ENV !== 'production') { const papabearAppRouter = require('papabear/AppRouter'); const gamemasterAppRouter = require('gamemaster/AppRouter'); const papabearconstants = require('papabearconstants'); const gamemasterconstants = require('gamemasterconstants'); Apps = [ { Name: papabearconstants.default.NAME, Component: papabearAppRouter.default, }, { Name: gamemasterconstants.default.NAME, Component: gamemasterAppRouter.default, } ]; } const enhance = process.env.NODE_ENV === 'production' ? compose( Component => withErrorBoundary( Component, withProps(() => ({ loading: false, logoText: ( <span>INSERT A COIN TO CONTINUE</span> ), statusText: null, children: ( <div> We have encountered an error, please refresh the page and try again. <hr /> Still hitting the same error? <a className="internalLink" href=""> <span className="fa fa-envelope" role="img" aria-label="e-mail" /> {' '} Tell us about it </a> </div> ), }))(LoadingPanel), ), ) : compose(); const emails = ['username@gmail.com', 'user02@gmail.com']; function ComplianceDialog(props) { const { onClose, selectedValue, open } = props; const handleClose = () => { onClose(selectedValue); }; const handleListItemClick = (value) => { onClose(value); }; return ( <Dialog onClose={handleClose} open={open}> <DialogTitle>Set backup account</DialogTitle> <DialogContent> <List sx={{ pt: 0 }}> {emails.map((email) => ( <ListItem disableGutters key={email}> <ListItemButton onClick={() => handleListItemClick(email)}> <ListItemAvatar> <Avatar sx={{ bgcolor: blue[100], color: blue[600] }}> <PersonIcon /> </Avatar> </ListItemAvatar> <ListItemText primary={email} /> </ListItemButton> </ListItem> ))} <ListItem disableGutters> <ListItemButton autoFocus onClick={() => handleListItemClick('addAccount')} > <ListItemAvatar> <Avatar> <AddIcon /> </Avatar> </ListItemAvatar> <ListItemText primary="Add account" /> </ListItemButton> </ListItem> </List> </DialogContent> </Dialog> ); } ComplianceDialog.propTypes = { onClose: PropTypes.func.isRequired, open: PropTypes.bool.isRequired, selectedValue: PropTypes.string.isRequired, }; const AppRouter = (props) => { const [open, setOpen] = useState(true); const [selectedValue, setSelectedValue] = useState(emails[1]); const handleClose = (value) => { setOpen(false); setSelectedValue(value); }; const prefix = "test"; return ( <div> <Typography variant="subtitle1" component="div"> Selected: {selectedValue} </Typography> <ComplianceDialog selectedValue={selectedValue} open={open} onClose={handleClose} /> <Switch> <Route path={`${prefix}/{PATHS.HOME}`} render={() => <Redirect to={prefix} />} /> <Route exact path={prefix} component={SplashPage} /> {Apps.map((app, index) => { return <Route key={index} path={`${prefix}/${app.Name}`} component={app.Component} />; })} <Route path={`${prefix}`} component={PageNotFoundView} /> </Switch> </div> ); }; export default enhance(AppRouter);
环境信息
System: OS: Linux 3.10 CentOS Linux 7 (Core) Binaries: Node: 16.20.0 - ~/.nvm/versions/node/v16.20.0/bin/node Yarn: Not Found npm: 7.24.2 - ~/node_modules/.bin/npm Browsers: Chrome: 118.0.5993.117 npmPackages: @emotion/react: latest => 11.11.1 @emotion/styled: latest => 11.11.0 @mui/base: 5.0.0-beta.20 @mui/core-downloads-tracker: 5.14.14 @mui/envinfo: ^2.0.12 => 2.0.12 @mui/icons-material: ^5.14.14 => 5.14.14 @mui/lab: ^5.0.0-alpha.149 => 5.0.0-alpha.149 @mui/material: ^5.14.14 => 5.14.14 @mui/private-theming: 5.14.14 @mui/styled-engine: 5.14.14 @mui/styles: ^5.14.14 => 5.14.14 @mui/system: 5.14.14 @mui/types: 7.2.6 @mui/utils: 5.14.14 @mui/x-data-grid: ^6.16.2 => 6.16.2 @mui/x-data-grid-generator: ^6.16.2 => 6.16.2 @mui/x-data-grid-premium: ^6.16.2 => 6.16.2 @mui/x-data-grid-pro: 6.16.2 @mui/x-license-pro: 6.10.2 @mui/x-tree-view: 6.0.0-alpha.1 @types/react: 17.0.66 react: ^18.2.0 => 18.2.0 react-dom: ^18.2.0 => 18.2.0 styled-components: ^5.3.10 => 5.3.11
问题分析与解决
核心原因
FocusTrap的栈溢出错误本质是HTMLDocument的contains方法被意外重写,导致MUI内部的焦点陷阱逻辑进入无限递归。常见触发场景:
- 项目引入旧版DOM polyfill(如IE兼容补丁),修改了原生
contains方法 - 第三方库(如老版本jQuery、自定义UI库)覆盖了
Element.prototype.contains或HTMLDocument.prototype.contains - 自定义全局DOM扩展与MUI的FocusTrap逻辑冲突
此外,代码中未定义的PATHS变量会导致路由路径解析错误,引发组件异常渲染,间接加重Dialog的状态管理问题。
修复步骤
1. 排查并修复全局DOM方法重写
搜索项目代码中是否存在类似以下的全局方法重写,若找到则移除或替换为标准实现:
// 错误示例:重写原生contains方法导致递归 HTMLDocument.prototype.contains = function() { // 错误逻辑 };
2. 修复未定义的PATHS变量
补充PATHS定义或直接使用字符串路径:
// 方式1:定义PATHS常量 const PATHS = { HOME: 'home' // 根据实际路由需求调整 }; // 方式2:直接使用字符串路径 <Route path={`${prefix}/home`} render={() => <Redirect to={prefix} />} />
3. 优化Dialog关闭逻辑
用useCallback包裹关闭函数,避免不必要的重渲染:
const AppRouter = (props) => { const [open, setOpen] = useState(true); const [selectedValue, setSelectedValue] = useState(emails[1]); // 用useCallback缓存函数 const handleClose = useCallback((value) => { setOpen(false); setSelectedValue(value); }, []); // ...其余代码不变 };
4. 升级MUI依赖(可选)
尝试升级@mui/material到最新稳定版,修复已知的FocusTrap bug:
npm update @mui/material
内容的提问来源于stack exchange,提问作者JavaScriptGirl
相关产品推荐
相关产品推荐

