React+TypeScript合并子菜单组件或共享状态实现互斥展开
解决React+TypeScript中子菜单互斥展开的问题
我用React+TypeScript开发,现在有两个功能几乎一样的组件DashboardsOpen和AnalysisOpen,区别只在于数据源(DashboardHeaders/AnalysisHeaders)和渲染的子组件(DashboardItems/AnalysisItems)。现在要求同一时间只能展开一个子菜单,但这两个组件状态独立,导致不同区域的子菜单能同时展开。我试过传标识字符串合并组件但报错失效,想实现组件合并或者状态共享来满足互斥需求。
原组件代码如下:
DashboardsOpen组件
const DashboardsOpen = () => { const items = DashboardHeaders; const [openId, setOpenId] = useState<number | null>(null); const toggleHandler = (id: number) => () => { setOpenId(openId => openId === id ? null : id); }; return ( <div className="Sub-Menu-Items-Container"> {items.map((item) => ( <div key={item.id}> <DashboardItems item={item} open={openId === item.id} onClick={toggleHandler(item.id)} /> </div> ))} </div> ) }
AnalysisOpen组件
const AnalysisOpen = () => { const items = AnalysisHeaders; const [openId, setOpenId] = useState<number | null>(null); const toggleHandler = (id: number) => () => { setOpenId(openId => openId === id ? null : id); }; return ( <div className="Sub-Menu-Items-Container"> {items.map((item) => ( <div key={item.id}> <AnalysisItems item={item} open={openId === item.id} onClick={toggleHandler(item.id)} /> </div> ))} </div> ) }
主容器代码
<div className="Side-Pannel-Navigation-Main-Wrapper-Open"> <Overview/> <div className="Side-Pannel-Section-Title">Dashboards</div> <DashboardsOpen/> <div className="Side-Pannel-Section-Title">Analysis</div> <SearchBar placeholder="Search...." data={SearchData}/> <AnalysisOpen/> </div>
解决方案
方案1:组件复用+状态提升(推荐)
把重复逻辑抽成通用组件,同时将状态提升到父容器,统一控制所有子菜单的展开状态,既解决重复代码问题,又实现互斥展开。
1. 创建通用组件CollapsibleMenu
import React from 'react'; // 定义菜单item的通用类型,根据实际业务补充属性 type MenuItem = { id: number; // 比如title、icon等公共字段 }; // 通用组件的props类型 type CollapsibleMenuProps<T extends MenuItem> = { items: T[]; // 传入要渲染的子组件 ItemComponent: React.ComponentType<{ item: T; open: boolean; onClick: () => void; }>; // 全局当前展开的ID currentOpenId: number | null; // 切换菜单的回调 onToggle: (id: number) => void; }; const CollapsibleMenu = <T extends MenuItem>({ items, ItemComponent, currentOpenId, onToggle, }: CollapsibleMenuProps<T>) => { return ( <div className="Sub-Menu-Items-Container"> {items.map((item) => ( <div key={item.id}> <ItemComponent item={item} open={currentOpenId === item.id} onClick={() => onToggle(item.id)} /> </div> ))} </div> ); }; export default CollapsibleMenu;
2. 在父容器中管理全局状态
修改主容器,把openId状态提到这里,统一处理切换逻辑:
import React, { useState } from 'react'; import CollapsibleMenu from './CollapsibleMenu'; import DashboardItems from './DashboardItems'; import AnalysisItems from './AnalysisItems'; import { DashboardHeaders, AnalysisHeaders } from './your-data-source'; // 导入数据源 const SideNavigation = () => { // 全局维护当前展开的菜单ID,null表示无展开项 const [currentOpenId, setCurrentOpenId] = useState<number | null>(null); const handleToggle = (id: number) => { // 点击已展开的项就关闭,否则切换到该项 setCurrentOpenId(prev => prev === id ? null : id); }; return ( <div className="Side-Pannel-Navigation-Main-Wrapper-Open"> <Overview /> <div className="Side-Pannel-Section-Title">Dashboards</div> <CollapsibleMenu items={DashboardHeaders} ItemComponent={DashboardItems} currentOpenId={currentOpenId} onToggle={handleToggle} /> <div className="Side-Pannel-Section-Title">Analysis</div> <SearchBar placeholder="Search...." data={SearchData} /> <CollapsibleMenu items={AnalysisHeaders} ItemComponent={AnalysisItems} currentOpenId={currentOpenId} onToggle={handleToggle} /> </div> ); }; export default SideNavigation;
这种方式的优势:
- 彻底复用组件逻辑,消除重复代码
- 状态统一管理,天然实现所有子菜单的互斥
- 泛型保证类型安全,避免组件和数据源不匹配的问题
方案2:React Context(适合复杂层级场景)
如果后续菜单结构变深,父容器传递状态过于繁琐,可以用Context共享状态:
1. 创建MenuContext
import React, { createContext, useContext, useState, ReactNode } from 'react'; type MenuContextType = { currentOpenId: number | null; toggleMenu: (id: number) => void; }; const MenuContext = createContext<MenuContextType | undefined>(undefined); export const MenuProvider = ({ children }: { children: ReactNode }) => { const [currentOpenId, setCurrentOpenId] = useState<number | null>(null); const toggleMenu = (id: number) => { setCurrentOpenId(prev => prev === id ? null : id); }; return ( <MenuContext.Provider value={{ currentOpenId, toggleMenu }}> {children} </MenuContext.Provider> ); }; // 自定义Hook简化Context调用 export const useMenuContext = () => { const context = useContext(MenuContext); if (!context) { throw new Error('useMenuContext必须在MenuProvider内部使用'); } return context; };
2. 改造通用组件使用Context
import React from 'react'; import { useMenuContext } from './MenuContext'; type MenuItem = { id: number; }; type CollapsibleMenuProps<T extends MenuItem> = { items: T[]; ItemComponent: React.ComponentType<{ item: T; open: boolean; onClick: () => void; }>; }; const CollapsibleMenu = <T extends MenuItem>({ items, ItemComponent, }: CollapsibleMenuProps<T>) => { const { currentOpenId, toggleMenu } = useMenuContext(); return ( <div className="Sub-Menu-Items-Container"> {items.map((item) => ( <div key={item.id}> <ItemComponent item={item} open={currentOpenId === item.id} onClick={() => toggleMenu(item.id)} /> </div> ))} </div> ); }; export default CollapsibleMenu;
3. 在根组件包裹MenuProvider
import { MenuProvider } from './MenuContext'; import SideNavigation from './SideNavigation'; const App = () => { return ( <MenuProvider> <SideNavigation /> </MenuProvider> ); };
之后主容器里直接用CollapsibleMenu即可,不用再手动传递状态和回调。
内容的提问来源于stack exchange,提问作者JPWilson
相关产品推荐
相关产品推荐

