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

React中如何动态控制未知数量折叠菜单的开闭状态?

动态控制多个折叠菜单的状态方案

当需要渲染未知数量的折叠菜单时,没法提前创建多个useState变量,这里有三种实用的解决思路:

1. 用对象存储每个项的展开状态

把状态改成一个对象,用每个数据项的唯一ID作为键,值对应该菜单的开闭状态(布尔值)。初始状态可以是空对象,或者根据需求设置默认展开的项。

示例代码:

// 初始化状态为空对象
const [openStates, setOpenStates] = useState({});

// 处理点击事件,接收当前项的ID
const handleItemClick = (itemId) => {
  setOpenStates(prev => ({
    ...prev,
    // 切换对应ID的状态,默认未展开则设为true
    [itemId]: !prev[itemId]
  }));
};

// 渲染从API获取的列表数据
{fetchedSurveys.map(survey => (
  <div key={survey.id}>
    <ListItemButton onClick={() => handleItemClick(survey.id)}>
      <ListItemIcon>
        <FolderIcon sx={{ color: theme => theme.palette.primary.main }} />
      </ListItemIcon>
      <ListItemText primary={survey.name} />
      {/* 根据当前项的状态显示图标 */}
      {openStates[survey.id] ? <ExpandLess /> : <ExpandMore />}
    </ListItemButton>
    <Collapse in={!!openStates[survey.id]} timeout='auto' unmountOnExit>
      {/* 这里替换成对应项的内容,比如survey的详情或子菜单 */}
      {isSurveysLoading || isSurveysRefetching ? (
        <LoadingProgress inSidebar />
      ) : isAdmin ? (
        // 可以根据survey传入对应的admin内容
        getAdminContent(survey)
      ) : (
        getClientContent(survey)
      )}
    </Collapse>
  </div>
))}

2. 用Set存储展开的项ID

如果大部分菜单默认是关闭的,用Set来记录已经展开的项ID会更高效。Set的has方法可以快速判断某个项是否展开。

示例代码:

// 初始化空Set
const [openIds, setOpenIds] = useState(new Set());

const handleItemClick = (itemId) => {
  setOpenIds(prev => {
    const newSet = new Set(prev);
    if (newSet.has(itemId)) {
      newSet.delete(itemId); // 已展开则关闭
    } else {
      newSet.add(itemId); // 未展开则打开
    }
    return newSet;
  });
};

// 渲染列表
{fetchedSurveys.map(survey => (
  <div key={survey.id}>
    <ListItemButton onClick={() => handleItemClick(survey.id)}>
      <ListItemIcon>
        <FolderIcon sx={{ color: theme => theme.palette.primary.main }} />
      </ListItemIcon>
      <ListItemText primary={survey.name} />
      {openIds.has(survey.id) ? <ExpandLess /> : <ExpandMore />}
    </ListItemButton>
    <Collapse in={openIds.has(survey.id)} timeout='auto' unmountOnExit>
      {/* 对应项的内容 */}
      {isSurveysLoading || isSurveysRefetching ? (
        <LoadingProgress inSidebar />
      ) : isAdmin ? (
        getAdminContent(survey)
      ) : (
        getClientContent(survey)
      )}
    </Collapse>
  </div>
))}

3. 封装独立的子组件

把单个折叠菜单的逻辑封装成独立组件,每个组件自己维护内部的开闭状态。这种方式代码解耦,父组件只需要传递数据和必要的props,不用管状态管理。

示例代码:

// 子组件:SingleSurveyMenu
const SingleSurveyMenu = ({ survey, isAdmin, isLoading }) => {
  const [isOpen, setIsOpen] = useState(false);

  const handleClick = () => {
    setIsOpen(prev => !prev);
  };

  return (
    <div>
      <ListItemButton onClick={handleClick}>
        <ListItemIcon>
          <FolderIcon sx={{ color: theme => theme.palette.primary.main }} />
        </ListItemIcon>
        <ListItemText primary={survey.name} />
        {isOpen ? <ExpandLess /> : <ExpandMore />}
      </ListItemButton>
      <Collapse in={isOpen} timeout='auto' unmountOnExit>
        {isLoading ? (
          <LoadingProgress inSidebar />
        ) : isAdmin ? (
          getAdminContent(survey)
        ) : (
          getClientContent(survey)
        )}
      </Collapse>
    </div>
  );
};

// 父组件中使用
{fetchedSurveys.map(survey => (
  <SingleSurveyMenu
    key={survey.id}
    survey={survey}
    isAdmin={isAdmin}
    isLoading={isSurveysLoading || isSurveysRefetching}
  />
))}

这三种方案各有优势:

  • 对象存储状态:适合需要明确记录每个项状态的场景,方便批量修改或初始化默认状态。
  • Set存储ID:轻量高效,适合默认全关的场景,代码更简洁。
  • 子组件独立管理:解耦性强,逻辑清晰,适合复杂的单个菜单逻辑。

内容的提问来源于stack exchange,提问作者psych0groov3

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 06:15:31