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

如何创建多层嵌套列表?解决点击展开项全部展开的问题

问题分析与解决方案

你现在碰到的问题很典型——所有可展开的列表项都共用了同一个open状态变量!不管你点击哪一个带expan: true的项,都会触发这个全局状态的切换,结果就是所有可展开项同时展开/收起,这肯定不是你想要的效果。

下面给你两种解决思路,都能完美解决这个问题:


思路1:用状态对象/数组追踪每个项的展开状态

我们可以把单个的open布尔值改成一个对象,用来存储每个可展开项的展开状态(用列表项的index作为key)。这样每个项的状态都是独立的,点击时只会切换对应项的状态。

修改后的AboutSkills组件代码:

const AboutSkills = () => {
  const classes = Styles()
  // 初始化空对象,每个index对应一个布尔值,默认都是false(收起状态)
  const [openStates, setOpenStates] = React.useState({});

  // 点击时传入当前项的index,只切换对应项的状态
  const handleClick = (index) => {
    setOpenStates(prevStates => ({
      ...prevStates,
      [index]: !prevStates[index] // 翻转当前项的状态,其他项保持不变
    }));
  };

  const outputList = () => (
    <>
      <List style={{color:'white'}} component="nav" className={classes.root} >
        {listItems.map((item, index) => {
          if(item.expan) {
            return <Fragment key={index}>
              <ListItem button onClick={() => handleClick(index)} className={classes.aboutList}>
                <ListItemIcon className={classes.aboutIcon}>
                  {item.listIcon}
                </ListItemIcon>
                <ListItemText primary={item.listText} />
                {/* 根据当前项的状态显示图标 */}
                {openStates[index] ? <ExpandLess /> : <ExpandMore />}
              </ListItem>
              {/* 同样,根据当前项的状态控制展开/收起 */}
              <Collapse in={openStates[index]} timeout="auto" unmountOnExit>
                <List component="div" disablePadding>
                  <ListItem button className={classes.nested}>
                    <ListItemIcon>
                      <StarBorder />
                    </ListItemIcon>
                    <ListItemText classes={{primary:classes.expanText}} primary={item.firstText} />
                  </ListItem>
                  { item.secondText && (
                    <ListItem button className={classes.nested}>
                      <ListItemIcon>
                        <StarBorder />
                      </ListItemIcon>
                      <ListItemText classes={{primary:classes.expanText}} primary={item.secondText} />
                    </ListItem>
                  )}
                  { item.thirdText && (
                    <ListItem button className={classes.nested}>
                      <ListItemIcon>
                        <StarBorder />
                      </ListItemIcon>
                      <ListItemText classes={{primary:classes.expanText}} primary={item.thirdText} />
                    </ListItem>
                  )}
                </List>
              </Collapse>
            </Fragment>
          } else {
            return <ListItem button className={classes.aboutList} key={index}>
              <ListItemIcon className={classes.aboutIcon}>
                {item.listIcon}
              </ListItemIcon>
              <ListItemText classes={{primary:classes.unExpanText}} primary={item.listText} />
            </ListItem>
          }
        })}
      </List>
    </>
  )
  return outputList()
}

思路2:把可展开项抽成独立组件(更推荐)

这种方法更符合React的组件化思想,把每个可展开的列表项做成一个独立的组件,让它自己管理自己的open状态,这样各个项之间完全隔离,不会互相影响。

第一步:创建ExpandableListItem组件

const ExpandableListItem = ({ item, classes }) => {
  // 每个组件自己维护独立的open状态
  const [open, setOpen] = React.useState(false);

  const handleClick = () => {
    setOpen(!open);
  };

  return (
    <Fragment>
      <ListItem button onClick={handleClick} className={classes.aboutList}>
        <ListItemIcon className={classes.aboutIcon}>
          {item.listIcon}
        </ListItemIcon>
        <ListItemText primary={item.listText} />
        {open ? <ExpandLess /> : <ExpandMore />}
      </ListItem>
      <Collapse in={open} timeout="auto" unmountOnExit>
        <List component="div" disablePadding>
          {/* 用&&替代三元表达式,代码更简洁 */}
          {item.firstText && (
            <ListItem button className={classes.nested}>
              <ListItemIcon><StarBorder /></ListItemIcon>
              <ListItemText classes={{primary:classes.expanText}} primary={item.firstText} />
            </ListItem>
          )}
          {item.secondText && (
            <ListItem button className={classes.nested}>
              <ListItemIcon><StarBorder /></ListItemIcon>
              <ListItemText classes={{primary:classes.expanText}} primary={item.secondText} />
            </ListItem>
          )}
          {item.thirdText && (
            <ListItem button className={classes.nested}>
              <ListItemIcon><StarBorder /></ListItemIcon>
              <ListItemText classes={{primary:classes.expanText}} primary={item.thirdText} />
            </ListItem>
          )}
        </List>
      </Collapse>
    </Fragment>
  );
};

第二步:在AboutSkills中使用这个组件

const AboutSkills = () => {
  const classes = Styles()

  const outputList = () => (
    <>
      <List style={{color:'white'}} component="nav" className={classes.root} >
        {listItems.map((item, index) => {
          if(item.expan) {
            // 直接渲染独立组件,传入item和classes
            return <ExpandableListItem key={index} item={item} classes={classes} />;
          } else {
            return <ListItem button className={classes.aboutList} key={index}>
              <ListItemIcon className={classes.aboutIcon}>
                {item.listIcon}
              </ListItemIcon>
              <ListItemText classes={{primary:classes.unExpanText}} primary={item.listText} />
            </ListItem>
          }
        })}
      </List>
    </>
  )
  return outputList()
}

额外提一句:你的listItems数组里有两个重复的ReactJs项,虽然不影响功能,但可以清理一下,让代码更整洁~

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 11:48:10