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
相关产品推荐
相关产品推荐

