递归组件中移除孙级菜单的问题排查与求助
配置驱动嵌套菜单层级联动问题解决方案
问题回顾
要开发配置驱动的嵌套上下文菜单,但层级超过2层时,根菜单切换只能关闭直接子菜单,孙级菜单无法同步关闭。比如点击主菜单的「Edit」时,「File」的子菜单消失,但「Open」的孙菜单还留在页面上。
核心原因
问题出在状态/属性没有贯穿整个菜单树:孙级菜单的显示状态仅依赖父级菜单的局部状态,根菜单的变更无法传递到深层节点,导致深层组件无法感知到状态变化而重新渲染。
优雅实现方案(无需第三方状态管理)
方案1:受控组件+路径追踪
通过顶层维护一个激活路径数组(比如['File', 'Open']表示当前激活到File下的Open菜单),所有层级菜单都根据这个路径判断是否显示。根菜单切换时直接更新路径,所有节点同步响应。
代码示例
import { useState } from 'react'; // 菜单配置示例 const menuConfig = [ { label: 'File', children: [ { label: 'Open', children: [ { label: 'Recent Files' }, { label: 'From Cloud' } ] }, { label: 'Save' } ] }, { label: 'Edit', children: [ { label: 'Undo' }, { label: 'Redo' } ] } ]; function MenuItem({ item, activePath, updatePath }) { // 判断当前菜单项是否处于激活路径中 const isActive = activePath.includes(item.label); const handleClick = () => { if (item.children) { // 切换根菜单时,重置路径;点击已有子菜单时,更新路径 const newPath = activePath[0] === item.label ? [] : [item.label]; updatePath(newPath); } else { // 无子女菜单,执行操作后关闭所有菜单 console.log(`执行操作:${item.label}`); updatePath([]); } }; return ( <div className="menu-item"> <button onClick={handleClick}>{item.label}</button> {/* 仅当当前项激活时,渲染子菜单,并传递子路径 */} {item.children && isActive && ( <div className="sub-menu"> {item.children.map(child => ( <MenuItem key={child.label} item={child} activePath={activePath.slice(1)} updatePath={(subPath) => updatePath([item.label, ...subPath])} /> ))} </div> )} </div> ); } function Menu() { const [activePath, setActivePath] = useState([]); return ( <div className="menu-bar"> {menuConfig.map(item => ( <MenuItem key={item.label} item={item} activePath={activePath} updatePath={setActivePath} /> ))} </div> ); }
方案2:React Context 全局状态传递
如果菜单层级很深,props传递会繁琐,可以用React Context把激活状态和更新方法全局共享,所有层级菜单直接从Context获取状态,无需层层传递。
代码示例
import { useState, createContext, useContext } from 'react'; const MenuContext = createContext(); function MenuProvider({ children }) { const [activePath, setActivePath] = useState([]); return ( <MenuContext.Provider value={{ activePath, setActivePath }}> {children} </MenuContext.Provider> ); } function MenuItem({ item }) { const { activePath, setActivePath } = useContext(MenuContext); const isActive = activePath.includes(item.label); const handleClick = () => { if (item.children) { const newPath = activePath[0] === item.label ? [] : [item.label]; setActivePath(newPath); } else { console.log(`执行操作:${item.label}`); setActivePath([]); } }; return ( <div className="menu-item"> <button onClick={handleClick}>{item.label}</button> {item.children && isActive && ( <div className="sub-menu"> {item.children.map(child => ( <MenuItem key={child.label} item={child} /> ))} </div> )} </div> ); } function Menu({ config }) { return ( <MenuProvider> <div className="menu-bar"> {config.map(item => ( <MenuItem key={item.label} item={item} /> ))} </div> </MenuProvider> ); }
效果说明
两种方案都能实现:点击根菜单切换时,所有层级的子菜单都会同步关闭,因为整个菜单树的显示状态都依赖顶层的activePath,根菜单变更会触发activePath更新,所有节点重新渲染并判断是否显示。
内容的提问来源于stack exchange,提问作者Rajesh Dixit
相关产品推荐
相关产品推荐

