React嵌套顶部菜单点击末级菜单项无法全关闭的问题求助
React嵌套菜单问题:点击末级选项无法关闭所有子菜单
问题说明
我参考网上多种实现思路,开发了如下React嵌套顶部菜单代码。目前代码除一个问题外均符合预期:点击递归菜单中的末级菜单项时,仅会关闭一级菜单,其余层级菜单仍处于显示状态,需多次点击菜单外部才能全部关闭。请问如何实现在用户点击菜单树中的末级菜单项时,关闭所有子菜单?
当前菜单显示效果:多级子菜单展开后,点击末级选项仅关闭一级菜单,下层子菜单仍保持展开状态。
原代码实现
menuItems.js
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;
MainMenu.js
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> ); }
MenuDropDown.js
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
相关产品推荐
相关产品推荐

