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

React嵌套顶部菜单点击末级菜单项无法全关闭的问题求助

React嵌套菜单问题:点击末级选项无法关闭所有子菜单

问题说明

我参考网上多种实现思路,开发了如下React嵌套顶部菜单代码。目前代码除一个问题外均符合预期:点击递归菜单中的末级菜单项时,仅会关闭一级菜单,其余层级菜单仍处于显示状态,需多次点击菜单外部才能全部关闭。请问如何实现在用户点击菜单树中的末级菜单项时,关闭所有子菜单?

当前菜单显示效果:多级子菜单展开后,点击末级选项仅关闭一级菜单,下层子菜单仍保持展开状态。


原代码实现

export const menuItems = [
  {
    title: "Home",
  },
  {
    title: "Services",
    submenu: [
      {
        title: "web design",
      },
      {
        title: "web development",
        submenu: [
          {
            title: "Frontend",
          },
          {
            title: "Backend",
            submenu: [
              {
                title: "NodeJS",
              },
              {
                title: "PHP",
              },
            ],
          },
        ],
      },
      {
        title: "SEO",
      },
    ],
  },
  {
    title: "About",
    submenu: [
      {
        title: "Who we are",
      },
      {
        title: "Our values",
      },
    ],
  },
];

App.js

import { Box } from "@mui/material";
import * as React from "react";
import { useState } from "react";
import MainMenu from "./MainMenu";

function App() {
  const [selectedMenuItem, setselectedMenuItem] = useState("");
  return (
    <>
      <MainMenu setselectedMenuItem={setselectedMenuItem} />
      <Box sx={{ bgcolor: "yellow", mt: "300px" }}>
        <p>
          Item Selected from Menu is{" "}
          <span style={{ color: "blue", fontSize: 32, fontWeight: "bold" }}>
            {selectedMenuItem}
          </span>
        </p>
      </Box>
    </>
  );
}

export default App;
import { Box } from "@mui/material";
import * as React from "react";
import MenuDropDown from "./MenuDropDown";
import { menuItems } from "./menuItems";

export default function MainMenu({ setselectedMenuItem }) {
  const depthLevel = 0;

  return (
    <Box sx={{ display: "flex", flexDirection: "row"}}>
      {menuItems.map((menu, index) => {
        return (
          <MenuDropDown
            key={index}
            menuDropDown={menuItems[index]}
            setselectedMenuItem={setselectedMenuItem}
            depthLevel = {depthLevel}
          />
        );
      })}
    </Box>
  );
}
import React from "react";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import ListItemText from "@mui/material/ListItemText";
import ListItemIcon from "@mui/material/ListItemIcon";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ArrowRightIcon from "@mui/icons-material/ArrowRight";

const MenuDropDown = ({ menuDropDown, setselectedMenuItem, depthLevel }) => {
  const [anchorEl, setAnchorEl] = React.useState(null);
  const open = Boolean(anchorEl);

  const handleClick = (event) => {
    setAnchorEl(event.currentTarget);
    setselectedMenuItem(event.currentTarget.innerText);
  };

  const handleClose = (event) => {
    setselectedMenuItem(event.currentTarget.innerText);
    setAnchorEl(null);
  };

  let currentDepthLevel = depthLevel;

  return (
    <div>
      {(() => {
        if ("submenu" in menuDropDown) {
          currentDepthLevel = depthLevel + 1;
          return (
            <div>
              <MenuItem sx={{ mr: 0, pr: 0 }} onClick={handleClick}>
                <ListItemText>{menuDropDown.title}</ListItemText>
                <ListItemIcon>
                  {currentDepthLevel === 1 ? (
                    <ArrowDropDownIcon />
                  ) : (
                    <ArrowRightIcon />
                  )}
                </ListItemIcon>
              </MenuItem>

              <Menu
                id="submenu"
                anchorEl={anchorEl}
                open={open}
                onClose={handleClose}
                anchorOrigin={
                  currentDepthLevel === 1
                    ? {
                        vertical: "bottom",
                        horizontal: "left",
                      }
                    : {
                        vertical: "bottom",
                        horizontal: "center",
                      }
                }
              >
                {menuDropDown.submenu.map((submenu, index) => {
                  if ("submenu" in submenu) {
                    return (
                      <MenuDropDown
                        key={index}
                        menuDropDown={submenu}
                        setselectedMenuItem={setselectedMenuItem}
                        depthLevel={currentDepthLevel}
                      />
                    );
                  }
                  return (
                    <MenuItem key={index} onClick={handleClose}>
                      {submenu.title}
                    </MenuItem>
                  );
                })}
              </Menu>
            </div>
          );
        } else {
          return (
            <MenuItem onClick={handleClick}>{menuDropDown.title}</MenuItem>
          );
        }
      })()}
    </div>
  );
};

export default MenuDropDown;

Package.json

{
  "name": "mongo-form",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/react": "^11.10.4",
    "@emotion/styled": "^11.10.4",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.3.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.3.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

解决方案

问题根源是每个MenuDropDown组件独立维护自身的anchorEl状态,点击末级菜单项时仅能关闭当前层级菜单,无法通知上层菜单同步关闭。通过状态提升到顶层统一管理可以彻底解决这个问题:

1. 顶层App组件统一管理菜单状态

修改App.js,添加状态记录展开的菜单路径,并提供关闭所有菜单的方法:

import { Box } from "@mui/material";
import * as React from "react";
import { useState } from "react";
import MainMenu from "./MainMenu";

function App() {
  const [selectedMenuItem, setSelectedMenuItem] = useState("");
  // 记录展开的菜单路径,例如["Services", "web development"]
  const [expandedPaths, setExpandedPaths] = useState([]);

  // 关闭所有菜单
  const closeAllMenus = () => {
    setExpandedPaths([]);
  };

  // 切换指定菜单的展开状态
  const toggleMenuPath = (path) => {
    setExpandedPaths(prev => {
      const pathIndex = prev.indexOf(path);
      if (pathIndex !== -1) {
        // 关闭当前及下层菜单
        return prev.slice(0, pathIndex);
      } else {
        // 展开当前菜单
        return [...prev, path];
      }
    });
  };

  return (
    <>
      <MainMenu 
        setSelectedMenuItem={setSelectedMenuItem}
        expandedPaths={expandedPaths}
        toggleMenuPath={toggleMenuPath}
        closeAllMenus={closeAllMenus}
      />
      <Box sx={{ bgcolor: "yellow", mt: "300px" }}>
        <p>
          选中的菜单项:
          <span style={{ color: "blue", fontSize: 32, fontWeight: "bold" }}>
            {selectedMenuItem}
          </span>
        </p>
      </Box>
    </>
  );
}

export default App;

2. 修改MainMenu传递状态与方法

import { Box } from "@mui/material";
import * as React from "react";
import MenuDropDown from "./MenuDropDown";
import { menuItems } from "./menuItems";

export default function MainMenu({ setSelectedMenuItem, expandedPaths, toggleMenuPath, closeAllMenus }) {
  const depthLevel = 0;

  return (
    <Box sx={{ display: "flex", flexDirection: "row"}}>
      {menuItems.map((menu, index) => {
        return (
          <MenuDropDown
            key={index}
            menuDropDown={menu}
            setSelectedMenuItem={setSelectedMenuItem}
            depthLevel={depthLevel}
            expandedPaths={expandedPaths}
            toggleMenuPath={toggleMenuPath}
            closeAllMenus={closeAllMenus}
            parentPath=""
          />
        );
      })}
    </Box>
  );
}

3. 重构MenuDropDown组件,使用顶层状态控制

import React from "react";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import ListItemText from "@mui/material/ListItemText";
import ListItemIcon from "@mui/material/ListItemIcon";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ArrowRightIcon from "@mui/icons-material/ArrowRight";

const MenuDropDown = ({ 
  menuDropDown, 
  setSelectedMenuItem, 
  depthLevel,
  expandedPaths,
  toggleMenuPath,
  closeAllMenus,
  parentPath
}) => {
  // 生成当前菜单的唯一路径标识
  const currentPath = parentPath ? `${parentPath}/${menuDropDown.title}` : menuDropDown.title;
  const isOpen = expandedPaths.includes(currentPath);
  const [anchorEl, setAnchorEl] = React.useState(null);

  // 同步顶层状态到本地锚点元素
  React.useEffect(() => {
    if (!isOpen) {
      setAnchorEl(null);
    }
  }, [isOpen]);

  const handleMenuToggle = (event) => {
    setAnchorEl(event.currentTarget);
    toggleMenuPath(currentPath);
    setSelectedMenuItem(menuDropDown.title);
  };

  // 末级菜单项点击时直接关闭所有菜单
  const handleFinalItemClick = (title) => {
    setSelectedMenuItem(title);
    closeAllMenus();
  };

  return (
    <div>
      {("submenu" in menuDropDown) ? (
        <div>
          <MenuItem sx={{ mr: 0, pr: 0 }} onClick={handleMenuToggle}>
            <ListItemText>{menuDropDown.title}</ListItemText>
            <ListItemIcon>
              {depthLevel === 1 ? (
                <ArrowDropDownIcon />
              ) : (
                <ArrowRightIcon />
              )}
            </ListItemIcon>
          </MenuItem>

          <Menu
            id="submenu"
            anchorEl={anchorEl}
            open={isOpen}
            onClose={() => toggleMenuPath(currentPath)}
            anchorOrigin={
              depthLevel === 1
                ? { vertical: "bottom", horizontal: "left" }
                : { vertical: "bottom", horizontal: "center" }
            }
          >
            {menuDropDown.submenu.map((submenu, index) => {
              if ("submenu" in submenu) {
                return (
                  <MenuDropDown
                    key={index}
                    menuDropDown={submenu}
                    setSelectedMenuItem={setSelectedMenuItem}
                    depthLevel={depthLevel + 1}
                    expandedPaths={expandedPaths}
                    toggleMenuPath={toggleMenuPath}
                    closeAllMenus={closeAllMenus}
                    parentPath={currentPath}
                  />
                );
              }
              return (
                <MenuItem key={index} onClick={() => handleFinalItemClick(submenu.title)}>
                  {submenu.title}
                </MenuItem>
              );
            })}
          </Menu>
        </div>
      ) : (
        <MenuItem onClick={() => handleFinalItemClick(menuDropDown.title)}>
          {menuDropDown.title}
        </MenuItem>
      )}
    </div>
  );
};

export default MenuDropDown;

核心逻辑

  • 状态统一管理:所有菜单的展开状态由顶层App的expandedPaths数组控制,每个子菜单通过路径标识判断是否展开。
  • 一键关闭所有菜单:末级菜单项点击时调用closeAllMenus清空expandedPaths,所有层级菜单同步关闭。
  • 状态同步:通过useEffect监听顶层状态变化,自动更新本地anchorEl,确保菜单显示状态与顶层逻辑一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 09:57:38