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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 21:25:24