如何为React菜单的子菜单项动态添加open状态?
解决方案
问题根源
你遇到的报错是因为违反了React Hook的规则:useState必须在函数组件的顶层调用,不能放在useEffect、循环或条件判断这类嵌套逻辑里。你之前尝试在useEffect里给菜单项动态添加状态,本质是在副作用回调里调用Hook,这是不被允许的。
方案一:集中管理子菜单展开状态
在侧边栏组件内维护一个状态对象,记录所有带子菜单的菜单项的展开状态,不需要修改/utils/Menus.js里的原始菜单结构(只需给每个子菜单加唯一标识id)。
1. 完善菜单定义(添加唯一ID)
// /utils/Menus.js export const adminMenu = [ { title: "仪表盘", path: "/dashboard", type: "link" }, { title: "用户管理", id: "user-management", // 唯一标识,用于绑定状态 type: "submenu", subitems: [ { title: "用户列表", path: "/users/list" }, { title: "新增用户", path: "/users/add" } ] } ]; export const regularUserMenu = [ { title: "个人中心", path: "/profile", type: "link" }, { title: "我的订单", id: "my-orders", type: "submenu", subitems: [ { title: "待发货", path: "/orders/pending" }, { title: "已完成", path: "/orders/completed" } ] } ];
2. 侧边栏组件实现
// /Sidenav.js import { useState } from 'react'; import { Collapse, Button } from 'react-bootstrap'; import { adminMenu, regularUserMenu } from './utils/Menus'; function Sidenav({ userType }) { // 根据用户类型获取对应菜单 const currentMenu = userType === 'admin' ? adminMenu : regularUserMenu; // 集中管理子菜单展开状态:key为菜单项id,value为布尔值 const [submenuStates, setSubmenuStates] = useState({}); // 切换指定子菜单的展开状态 const toggleSubmenu = (menuId) => { setSubmenuStates(prev => ({ ...prev, [menuId]: !prev[menuId] })); }; // 渲染单个菜单项 const renderMenuItem = (item) => { if (item.type === 'link') { return ( <Button key={item.path} variant="link" href={item.path} className="w-100 text-start"> {item.title} </Button> ); } if (item.type === 'submenu') { const isOpen = submenuStates[item.id] || false; return ( <div key={item.id} className="mb-2"> <Button onClick={() => toggleSubmenu(item.id)} variant="light" className="w-100 text-start d-flex justify-content-between align-items-center" > {item.title} <span>{isOpen ? '▼' : '▶'}</span> </Button> <Collapse in={isOpen}> <div className="ms-4 mt-1"> {item.subitems.map(subitem => ( <Button key={subitem.path} variant="link" href={subitem.path} className="w-100 text-start"> {subitem.title} </Button> ))} </div> </Collapse> </div> ); } return null; }; return ( <div className="sidenav p-3 border-end"> {currentMenu.map(renderMenuItem)} </div> ); } export default Sidenav;
方案二:独立子菜单组件(解耦状态管理)
创建专门的Submenu组件,让每个子菜单自己维护展开状态,侧边栏组件只需负责渲染菜单结构,无需关心状态逻辑。
1. 子菜单组件
// /components/Submenu.js import { useState } from 'react'; import { Collapse, Button } from 'react-bootstrap'; function Submenu({ title, subitems }) { const [isOpen, setIsOpen] = useState(false); return ( <div className="mb-2"> <Button onClick={() => setIsOpen(!isOpen)} variant="light" className="w-100 text-start d-flex justify-content-between align-items-center" > {title} <span>{isOpen ? '▼' : '▶'}</span> </Button> <Collapse in={isOpen}> <div className="ms-4 mt-1"> {subitems.map(subitem => ( <Button key={subitem.path} variant="link" href={subitem.path} className="w-100 text-start"> {subitem.title} </Button> ))} </div> </Collapse> </div> ); } export default Submenu;
2. 侧边栏组件简化版
// /Sidenav.js import { Button } from 'react-bootstrap'; import { adminMenu, regularUserMenu } from './utils/Menus'; import Submenu from './components/Submenu'; function Sidenav({ userType }) { const currentMenu = userType === 'admin' ? adminMenu : regularUserMenu; const renderMenuItem = (item) => { if (item.type === 'link') { return ( <Button key={item.path} variant="link" href={item.path} className="w-100 text-start mb-2"> {item.title} </Button> ); } if (item.type === 'submenu') { return <Submenu key={item.title} title={item.title} subitems={item.subitems} />; } return null; }; return ( <div className="sidenav p-3 border-end"> {currentMenu.map(renderMenuItem)} </div> ); } export default Sidenav;
方案对比
- 方案一:适合需要全局控制子菜单状态的场景(比如添加“折叠所有”按钮),状态集中管理,便于统一操作。
- 方案二:代码解耦度更高,每个子菜单独立维护状态,侧边栏逻辑更简洁,适合大多数常规场景。
内容的提问来源于stack exchange,提问作者RBFraphael
相关产品推荐
相关产品推荐

