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

如何在React多级菜单中从父菜单检测激活的子菜单?

React多级菜单激活状态问题

使用单个状态记录激活菜单可以轻松实现单级菜单:

单级菜单实现代码

App组件

import React from 'react';

import { Menu } from './Menu';

export const App = () => {
  const [active, setActive] = React.useState<React.ReactNode>(null);
  return (
    <>
      <Menu label="A" active={active} setActive={setActive}>
        <Menu label="A.A" active={active} setActive={setActive} />
        <Menu label="A.B" active={active} setActive={setActive} />
      </Menu>
      <Menu label="B" active={active} setActive={setActive}>
        <Menu label="B.A" active={active} setActive={setActive} />
        <Menu label="B.B" active={active} setActive={setActive} />
      </Menu>
    </>
  );
};
import React from 'react';

type Props = {
  label: string;
  active?: HTMLElement;
  setActive?: (active: HTMLElement) => void;
  children?: React.ReactNode;
};

export const Menu = ({ label, active, setActive, children }: Props) => {
  const ref = React.useRef<HTMLDivElement>(null);
  const onClick = () => setActive?.(children && ref.current);
  return (
    <div ref={ref}>
      <button type="button" onClick={onClick}>
        {label}
      </button>
      {active && active === ref.current && children}
    </div>
  );
};

但实现多级菜单时,子菜单激活时父菜单需要保持激活状态,当前方案无法实现:

多级菜单尝试代码

App组件

import React from 'react';

import { Menu } from './Menu';

export const App = () => {
  const [active, setActive] = React.useState<React.ReactNode>(null);
  return (
    <>
      <Menu label="A" active={active} setActive={setActive}>
        <Menu label="A.A" active={active} setActive={setActive}>
          <Menu label="A.A.A" active={active} setActive={setActive} />
          <Menu label="A.A.B" active={active} setActive={setActive} />
        </Menu>
        <Menu label="A.B" active={active} setActive={setActive}>
          <Menu label="A.B.A" active={active} setActive={setActive} />
          <Menu label="A.B.B" active={active} setActive={setActive} />
        </Menu>
      </Menu>
      <Menu label="B" active={active} setActive={setActive}>
        <Menu label="B.A" active={active} setActive={setActive}>
          <Menu label="B.A.A" active={active} setActive={setActive} />
          <Menu label="B.A.B" active={active} setActive={setActive} />
        </Menu>
        <Menu label="B.B" active={active} setActive={setActive}>
          <Menu label="B.B.A" active={active} setActive={setActive} />
          <Menu label="B.B.B" active={active} setActive={setActive} />
        </Menu>
      </Menu>
    </>
  );
};
import React from 'react';

type Props = {
  label: string;
  active?: HTMLElement;
  setActive?: (active: HTMLElement) => void;
  children?: React.ReactNode;
};

export const Menu = ({ label, active, setActive, children }: Props) => {
  const ref = React.useRef<HTMLDivElement>(null);
  const onClick = () => setActive?.(children && ref.current);
  return (
    <div ref={ref}>
      <button type="button" onClick={onClick}>
        {label}
      </button>
      {active && Array.isArray(children) && children.includes(active) && children}  // 无法生效!
    </div>
  );
};

问题在于children.includes(active)始终返回false,因为children是React.ReactNode类型(虚拟DOM节点),而active是HTMLElement类型(真实DOM元素),两者无法直接比较。


解决方案

方案1:利用DOM层级关系判断

直接通过DOM的contains方法,判断激活元素是否是当前菜单元素的后代,无需处理类型转换:

修改后的Menu组件:

import React from 'react';

type Props = {
  label: string;
  active?: HTMLElement | null;
  setActive?: (active: HTMLElement | null) => void;
  children?: React.ReactNode;
};

export const Menu = ({ label, active, setActive, children }: Props) => {
  const ref = React.useRef<HTMLDivElement>(null);
  
  const onClick = () => {
    if (!ref.current) return;
    
    // 当前菜单已激活(自身或后代元素激活),点击则关闭
    if (active && (ref.current === active || ref.current.contains(active))) {
      setActive?.(null);
    } else {
      // 未激活则设置当前菜单为激活状态
      setActive?.(children ? ref.current : null);
    }
  };

  // 判断是否展示子菜单:激活元素是当前菜单或其后代
  const shouldShowChildren = active && ref.current && (ref.current === active || ref.current.contains(active));

  return (
    <div ref={ref}>
      <button type="button" onClick={onClick}>
        {label}
      </button>
      {shouldShowChildren && children}
    </div>
  );
};

同时更新App组件的状态类型:

const [active, setActive] = React.useState<HTMLElement | null>(null);

方案2:跟踪激活路径标识

放弃存储DOM元素,改用菜单的唯一标识路径(比如label层级)来跟踪激活状态,更适合复杂菜单场景:

修改后的App组件:

import React from 'react';

import { Menu } from './Menu';

export const App = () => {
  // 存储激活的菜单路径数组,例如 ["A", "A.A", "A.A.A"]
  const [activePath, setActivePath] = React.useState<string[]>([]);
  return (
    <>
      <Menu 
        label="A" 
        activePath={activePath} 
        setActivePath={setActivePath}
        currentPath="A"
      >
        <Menu 
          label="A.A" 
          activePath={activePath} 
          setActivePath={setActivePath}
          currentPath="A.A"
        >
          <Menu 
            label="A.A.A" 
            activePath={activePath} 
            setActivePath={setActivePath}
            currentPath="A.A.A"
          />
          <Menu 
            label="A.A.B" 
            activePath={activePath} 
            setActivePath={setActivePath}
            currentPath="A.A.B"
          />
        </Menu>
        <Menu 
          label="A.B" 
          activePath={activePath} 
          setActivePath={setActivePath}
          currentPath="A.B"
        >
          <Menu 
            label="A.B.A" 
            activePath={activePath} 
            setActivePath={setActivePath}
            currentPath="A.B.A"
          />
          <Menu 
            label="A.B.B" 
            activePath={activePath} 
            setActivePath={setActivePath}
            currentPath="A.B.B"
          />
        </Menu>
      </Menu>
      <Menu 
        label="B" 
        activePath={activePath} 
        setActivePath={setActivePath}
        currentPath="B"
      >
        <Menu 
          label="B.A" 
          activePath={activePath} 
          setActivePath={setActivePath}
          currentPath="B.A"
        >
          <Menu 
            label="B.A.A" 
            activePath={activePath} 
            setActivePath={setActivePath}
            currentPath="B.A.A"
          />
          <Menu 
            label="B.A.B" 
            activePath={activePath} 
            setActivePath={setActivePath}
            currentPath="B.A.B"
          />
        </Menu>
        <Menu 
          label="B.B" 
          activePath={activePath} 
          setActivePath={setActivePath}
          currentPath="B.B"
        >
          <Menu 
            label="B.B.A" 
            activePath={activePath} 
            setActivePath={setActivePath}
            currentPath="B.B.A"
          />
          <Menu 
            label="B.B.B" 
            activePath={activePath} 
            setActivePath={setActivePath}
            currentPath="B.B.B"
          />
        </Menu>
      </Menu>
    </>
  );
};

修改后的Menu组件:

import React from 'react';

type Props = {
  label: string;
  activePath: string[];
  setActivePath: (path: string[]) => void;
  currentPath: string;
  children?: React.ReactNode;
};

export const Menu = ({ label, activePath, setActivePath, currentPath, children }: Props) => {
  // 当前菜单是否处于激活路径中
  const isInActivePath = activePath.includes(currentPath);
  // 当前菜单是否是最后激活的节点
  const isLastActive = activePath.at(-1) === currentPath;

  const onClick = () => {
    if (isLastActive) {
      // 点击已激活的菜单,收起自身及后代
      const newPath = activePath.slice(0, activePath.indexOf(currentPath));
      setActivePath(newPath);
    } else {
      // 点击未激活的菜单,激活自身及父级路径
      const newPath = [...activePath.slice(0, activePath.indexOf(currentPath) + 1), currentPath];
      setActivePath(newPath);
    }
  };

  return (
    <div>
      <button type="button" onClick={onClick}>
        {label}
      </button>
      {isInActivePath && children}
    </div>
  );
};

内容的提问来源于stack exchange,提问作者Géry Ogam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 05:25:56