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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 00:36:04