如何在React映射渲染的组件上使用Menu/Popover并解决弹窗重叠问题
问题根因
你遇到的菜单重叠问题确实是所有列表项共用同一全局状态导致的:只要popState.mouseY不为空,遍历生成的所有Menu组件的open属性都会变为true,全部渲染在同一个坐标位置就会出现重叠。
通用实现方案
这类批量渲染组件的状态控制通常有两种成熟方案,可根据业务复杂度选择:
方案1:扩展全局状态,增加激活项标识
适合菜单逻辑简单、不想拆分组件的场景,只需要在状态中新增当前激活的菜单ID,打开时校验ID匹配即可。
修改后的代码示例:
// 状态新增activeId字段存储当前激活的分类ID const popInitialPos = { mouseX: null, mouseY: null, activeId: null as null | number }; const [popState, setPopState] = React.useState(popInitialPos); // 打开菜单时传入当前分类的ID const openPopCategory = (event: React.MouseEvent<HTMLDivElement>, categoryId: number) => { event.preventDefault(); setPopState({ mouseX: event.clientX - 2, mouseY: event.clientY - 4, activeId: categoryId }); }; const closePopCategory = () => { setPopState(popInitialPos); }; // 遍历渲染部分修改 {categories?.map( (c)=>{ if( c.id>=5){ return( <Link to={{pathname:"/list/"+c.id}} className={classes.linkItem} key={c.id}> <div className={classes.item} onContextMenu={(e)=>openPopCategory(e, c.id)}> <Dns className={classes.icon} /> <Typography className={classes.text}>{c.name}</Typography> <Typography className={classes.text} style={{marginLeft:"auto"}}> {todos.filter((todo)=>todo.categories?.includes(c.id)&&todo.completed===false).length ===0 ? null : todos.filter((todo)=>todo.categories?.includes(c.id)&&todo.completed===false).length } </Typography> <Menu keepMounted // 只有当前分类ID和激活ID匹配时才打开菜单 open={popState.mouseY !== null && popState.activeId === c.id} onClose={closePopCategory} anchorReference="anchorPosition" anchorPosition={ popState.mouseY !== null && popState.mouseX !== null ? { top: popState.mouseY, left: popState.mouseX } : undefined } > <MenuItem onClick={closePopCategory}>{c.name}</MenuItem> </Menu> </div> </Link> ) } })}
方案2:封装独立列表项组件,状态内部自治
更推荐的通用方案,符合单一职责的组件设计原则,每个列表项自己维护菜单状态,和父组件完全解耦,后续扩展逻辑也更方便。
示例代码:
// 抽离独立的分类项组件,自己管理菜单状态 interface CategoryItemProps { c: Category // 替换为你实际的分类类型定义 todos: Todo[] // 替换为你实际的待办项类型定义 } const CategoryItem = ({ c, todos }: CategoryItemProps) => { const popInitialPos = { mouseX: null, mouseY: null, }; const [popState, setPopState] = React.useState(popInitialPos); const openPopCategory = (event: React.MouseEvent<HTMLDivElement>) => { event.preventDefault(); setPopState({ mouseX: event.clientX - 2, mouseY: event.clientY - 4, }); }; const closePopCategory = () => { setPopState(popInitialPos); }; const unfinishedCount = todos.filter(todo => todo.categories?.includes(c.id) && !todo.completed).length; return ( <Link to={{pathname:"/list/"+c.id}} className={classes.linkItem}> <div className={classes.item} onContextMenu={openPopCategory}> <Dns className={classes.icon} /> <Typography className={classes.text}>{c.name}</Typography> <Typography className={classes.text} style={{marginLeft:"auto"}}> {unfinishedCount === 0 ? null : unfinishedCount} </Typography> <Menu keepMounted open={popState.mouseY !== null} onClose={closePopCategory} anchorReference="anchorPosition" anchorPosition={ popState.mouseY !== null && popState.mouseX !== null ? { top: popState.mouseY, left: popState.mouseX } : undefined } > <MenuItem onClick={closePopCategory}>{c.name}</MenuItem> </Menu> </div> </Link> ) } // 父组件遍历部分直接调用即可 {categories?.map(c => c.id >=5 ? <CategoryItem key={c.id} c={c} todos={todos} /> : null)}
两种方案都可以解决重叠问题,如果每个菜单的逻辑比较独立优先选方案2,代码可维护性更高;如果菜单需要和父组件做大量联动,方案1的全局状态管理会更方便。
内容的提问来源于stack exchange,提问作者Kai021195
相关产品推荐
相关产品推荐

