React路由切换时如何保持Material-UI侧边菜单展开状态?
解决React Material-UI侧边菜单路由跳转后收起的问题
核心原因
你遇到的问题大概率是菜单展开状态存在组件内部的local state中,路由跳转时(history.push()触发),侧边菜单组件可能会重新渲染或重新挂载,导致局部state被重置为初始的关闭状态,所以菜单会自动收起。
解决方法
方法1:将展开状态迁移到全局状态或持久化存储
把菜单的展开状态从组件内部state提升到全局状态管理(比如Context API、Redux),或者存入localStorage做持久化,这样路由切换时状态不会丢失。
示例(用localStorage持久化):
import { useState, useEffect } from 'react'; import { List, ListItem, Collapse, ExpandLess, ExpandMore } from '@material-ui/core'; import { useHistory } from 'react-router-dom'; const SideMenu = () => { const history = useHistory(); // 初始化时从localStorage读取上次的展开状态 const [expandedKey, setExpandedKey] = useState(() => { return localStorage.getItem('expandedMenuKey') || ''; }); // 状态更新时同步存入localStorage useEffect(() => { localStorage.setItem('expandedMenuKey', expandedKey); }, [expandedKey]); const toggleMenu = (key) => { setExpandedKey(prev => prev === key ? '' : key); }; return ( <List> {/* 主菜单项 */} <ListItem button onClick={() => toggleMenu('dashboard')}> Dashboard {expandedKey === 'dashboard' ? <ExpandLess /> : <ExpandMore />} </ListItem> {/* 子菜单 */} <Collapse in={expandedKey === 'dashboard'} unmountOnExit> <List component="div" disablePadding> <ListItem button onClick={() => history.push('/dashboard/overview')}> Overview </ListItem> <ListItem button onClick={() => history.push('/dashboard/analytics')}> Analytics </ListItem> </List> </Collapse> {/* 其他主菜单 */} </List> ); };
方法2:基于当前路由自动匹配展开菜单
不依赖点击状态,而是根据当前路由路径判断应该展开哪个主菜单,这种方法更符合路由驱动UI的理念,用户刷新页面或直接访问子路由时也能自动展开对应菜单。
示例:
import { useLocation } from 'react-router-dom'; import { List, ListItem, Collapse, ExpandLess, ExpandMore } from '@material-ui/core'; import { useHistory } from 'react-router-dom'; const SideMenu = () => { const history = useHistory(); const location = useLocation(); // 定义主菜单与子路由的映射关系 const menuRouteMap = { dashboard: ['/dashboard', '/dashboard/overview', '/dashboard/analytics'], settings: ['/settings', '/settings/profile', '/settings/privacy'] }; // 匹配当前路由对应的主菜单key const currentExpandedKey = Object.keys(menuRouteMap).find(key => menuRouteMap[key].includes(location.pathname) ); return ( <List> <ListItem button onClick={() => { // 可选:点击主菜单时切换展开/收起,或直接跳转到默认子路由 currentExpandedKey === 'dashboard' ? setExpandedKey('') : setExpandedKey('dashboard') }}> Dashboard {currentExpandedKey === 'dashboard' ? <ExpandLess /> : <ExpandMore />} </ListItem> <Collapse in={currentExpandedKey === 'dashboard'} unmountOnExit> <List component="div" disablePadding> <ListItem button onClick={() => history.push('/dashboard/overview')}> Overview </ListItem> <ListItem button onClick={() => history.push('/dashboard/analytics')}> Analytics </ListItem> </List> </Collapse> {/* 其他主菜单同理 */} </List> ); };
额外注意事项
- 如果你的侧边菜单组件被包裹在
Route组件内部,或者路由切换会导致菜单的父组件重新渲染,那么局部state必然会丢失,此时必须采用全局状态或路由匹配的方案。 - 若想保留局部state,可以尝试用
React.memo包裹菜单组件,避免不必要的重渲染,但这种方法仅适用于父组件重渲染不会导致菜单组件卸载的场景,可靠性不如前两种方法。
内容的提问来源于stack exchange,提问作者64Bit1990
相关产品推荐
相关产品推荐

