如何在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> </> ); };
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> </> ); };
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
相关产品推荐
相关产品推荐

