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

关闭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?&nbsp;&nbsp;
            <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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 10:45:54