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

React导航组件树需求:实现关闭已展开父节点功能

实现React导航组件的单节点展开功能

现有组件背景

我已经创建了一个用于导航的React组件树,需求是实现:当展开新的父节点时,自动关闭之前已展开的父节点。

现有配置与代码

导航配置数据

let navConfigData = {
  isDashboardVisible: true,
  tierOneLinks: [
    {
      name: "First Page",
      iconName: "star",
      iconAbbreviation: "FP",
      tierTwoItems: [
        {
          name: "Tier 2 Page",
          iconName: "star",
          iconAbbreviation: "FP",
          tierThreeItems: [
            {
              name: "Tier 3 Page",
              iconName: "star",
              iconAbbreviation: "FP",
              path: "#"
            }
          ]
        },
        {
          name: "Tier 2 sec Page",
          iconName: "star",
          iconAbbreviation: "FP",
          path: "#"
        }
      ]
    },
    {
      name: "Second Page",
      iconName: "",
      iconAbbreviation: "SP",
      path: "#"
    },
    {
      name: "Third Page",
      iconAbbreviation: "TP",
      path: "#",
      tierTwoItems: [
        {
          name: "Tier 2 Page",
          iconName: "star",
          iconAbbreviation: "FP",
          tierThreeItems: [
            {
              name: "Tier 3 Page",
              iconName: "star",
              iconAbbreviation: "FP",
              path: "#"
            }
          ]
        },
        {
          name: "Tier 2 sec Page",
          iconName: "star",
          iconAbbreviation: "FP",
          path: "#"
        }
      ]
    },
    {
      name: "Fourth Page",
      iconName: "send",
      iconAbbreviation: "FO",
      path: "#"
    }
  ],
  supportLinks: [
    {
      name: "First Support Page",
      iconName: "settings",
      iconAbbreviation: "FI",
      path: "#"
    },
    {
      name: "Second Support Page",
      iconName: "security",
      iconAbbreviation: "SE",
      path: "#"
    },
    {
      name: "Third Support Page",
      iconName: "help",
      iconAbbreviation: "TH",
      path: "#"
    }
  ]
};

组件代码

class NavigationDrawerTierThree extends React.Component {
  onClick = evt => {
    this.props.onSelect(this.props.tierThree.name);
  };

  render() {
    const { tierThree, selectedTierThreeLink } = this.props;
    debugger;
    return (
      <a className="mdc-list-item my-nav-drawer__tier-three" onClick={this.onClick} >
        <span className="my-nav-drawer__tier-two-label">
          {tierThree.name}
        </span>
      </a>
    );
  }
}

class NavigationDrawerTierTwo extends React.Component {
  // static propTypes = {
  // tierTwo: object.isRequired,
  // showNavIcon: bool,
  // onSelect: func.isRequired,
  // selectedTierTwoLink: string.isRequired
  // };
  state = { tierTwoIsExpanded: false };

  onClick = evt => {
    debugger;
    this.props.onSelect(this.props.tierTwo.name, "tier2");
    const { tierTwoIsExpanded } = this.state;
    this.setState({ tierTwoIsExpanded: !tierTwoIsExpanded });
  };

  getArrowIcon = () => {
    return this.state.tierTwoIsExpanded ? (
      <span class="material-icons cdk-nav-drawer__tier-two-icon">></span>
    ) : (
      <span class="material-icons cdk-nav-drawer__tier-two-icon"> ^ </span>
    );
  };

  getAnchorClassBasedOnTier = () => {
    return this.props.showNavIcon ? "cdk-nav-drawer__tier-two" : "cdk-nav-drawer__tier-three";
  };

  getLabelClassBasedOnTier = () => {
    return this.props.showNavIcon ? "cdk-nav-drawer__tier-two-label" : "cdk-nav-drawer__tier-three-label";
  };

  render() {
    const { tierTwo, showNavIcon, selectedTierTwoLink } = this.props;
    const { tierTwoIsExpanded } = this.state;
    debugger;
    return (
      <div>
        <a className="mdc-list-item my-nav-drawer__tier-two" onClick={this.onClick} >
          {showNavIcon && this.getArrowIcon()}
          <span className="my-nav-drawer__tier-two-label">
            {tierTwo.name}
          </span>
        </a>
        {tierTwoIsExpanded && this.props.children}
      </div>
    );
  }
}

class NavigationDrawerTierOneAnchor extends React.Component{
  // static propTypes = {
  // tierOne: object.isRequired,
  // onSelect: func.isRequired,
  // selectedTierOneLink: string.isRequired
  // };
  // state = { tierIsExpanded: false };

  onClick = evt => {
    this.props.onSelect(this.props.tierOne.name, "tier1");
    // const { tierIsExpanded } = this.state;
    // this.setState({ tierIsExpanded: !tierIsExpanded });
  };

  render(){
    const { tierOne, selectedTierOneLink, expanded } = this.props;
    return(
      <div>
        <a className="mdc-list-item my-nav-drawer__tier-one" href={tierOne.path} onClick={this.onClick} ref={a => { this.anchor = a; }} >
          <span className="cdk-nav-drawer__tier-one-label">
            {tierOne.name}
          </span>
        </a>
        {expanded && this.props.children}
      </div>
    );
  }
}

class NavigationDrawerAppTiers extends React.Component {
  // static propTypes = {
  // tierOneArray: array.isRequired,
  // onToggleExpand: func.isRequired,
  // onSelect: func.isRequired,
  // selectedTierOneLink: string.isRequired,
  // selectedTierTwoLink: string.isRequired,
  // selectedTierThreeLink: string.isRequired,
  // expandedTierOneName: string.isRequired
  // };

  render(){
    const { tierOneArray, onSelect, selectedTierOneLink, selectedTierTwoLink, selectedTierThreeLink, expandedTierOneName } = this.props;
    return (
      <div className="mdc-drawer__content my-nav-drawer__app-tiers">
        <nav className="mdc-list">
          {tierOneArray.map((tierOneLink, index) => (
            <NavigationDrawerTierOneAnchor
              tierOne={tierOneLink}
              onSelect={onSelect}
              key={index}
              expanded={tierOneLink.name === this.props.expandedTierOneName}
              selectedTierOneLink={selectedTierOneLink}
            >
              {(tierOneLink.tierTwoItems || []).map((tierTwoItem, index) => (
                <NavigationDrawerTierTwo
                  tierTwo={tierTwoItem}
                  onSelect={onSelect}
                  selected

功能实现方案

核心思路是将展开状态从子组件移到父组件进行集中管理,这样父组件可以全局控制所有节点的展开状态,实现点击新节点时自动关闭旧节点的效果。具体步骤如下:

1. 在父组件中维护展开状态

找到包含NavigationDrawerAppTiers的上层组件(或者直接修改NavigationDrawerAppTiers),添加状态来记录当前展开的TierOne节点名称:

state = {
  expandedTierOneName: null, // 初始不展开任何节点,可设置默认值如"First Page"
  selectedTierOneLink: '',
  selectedTierTwoLink: '',
  selectedTierThreeLink: ''
};

2. 添加状态更新方法

在父组件中添加处理TierOne节点展开/关闭的方法,逻辑是:如果点击的节点已经是展开状态,就关闭它;否则展开该节点并关闭其他节点:

handleTierOneExpand = (tierOneName) => {
  this.setState(prevState => ({
    expandedTierOneName: prevState.expandedTierOneName === tierOneName ? null : tierOneName,
    // 可选:展开新TierOne时自动关闭所有TierTwo节点
    expandedTierTwoName: null
  }));
};

3. 传递状态与回调给子组件

修改NavigationDrawerAppTiers的props传递,把父组件的expandedTierOneName和handleTierOneExpand传递下去:

<NavigationDrawerAppTiers
  tierOneArray={navConfigData.tierOneLinks}
  onSelect={this.handleSelect}
  expandedTierOneName={this.state.expandedTierOneName}
  onToggleTierOneExpand={this.handleTierOneExpand}
  selectedTierOneLink={this.state.selectedTierOneLink}
  // ...其他props
/>

然后在NavigationDrawerAppTiers的render方法中,把展开回调传递给每个NavigationDrawerTierOneAnchor:

<NavigationDrawerTierOneAnchor
  tierOne={tierOneLink}
  onSelect={onSelect}
  onExpand={() => this.props.onToggleTierOneExpand(tierOneLink.name)}
  key={index}
  expanded={tierOneLink.name === this.props.expandedTierOneName}
  selectedTierOneLink={selectedTierOneLink}
>

4. 修改TierOneAnchor的点击逻辑

更新NavigationDrawerTierOneAnchor的onClick方法,调用父组件的展开回调,并阻止默认跳转(如果需要):

onClick = evt => {
  evt.preventDefault(); // 阻止a标签默认跳转,优先处理展开逻辑
  this.props.onSelect(this.props.tierOne.name, "tier1");
  this.props.onExpand(); // 触发父组件的状态更新
};

5. 可选:TierTwo节点的单展开实现

如果需要TierTwo节点也实现点击新节点关闭旧节点的功能,重复上述逻辑:

  • 在父组件添加expandedTierTwoName状态
  • 添加handleTierTwoExpand方法
  • 传递状态和回调给NavigationDrawerTierTwo组件
  • 修改NavigationDrawerTierTwo的点击逻辑,移除自身的state,改用父组件传递的状态

总结

通过集中式状态管理,我们把原本分散在子组件的展开状态统一到父组件,这样就能轻松实现"展开新节点自动关闭旧节点"的需求,同时也让组件状态更易维护和调试。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 04:28:34