React+Material-UI中ProSidebar双击激活路由的问题排查
解决React Router侧边栏菜单项需双击激活的问题
我在使用React Router的React应用中遇到导航问题:必须双击侧边栏菜单项才能使其激活并触发对应路由。已确认路由配置正确,但问题依旧。
App组件代码
import { ColorModeContext, useMode } from "./theme"; import { CssBaseline, ThemeProvider } from "@mui/material"; import { Routes, Route } from "react-router-dom"; import Topbar from "./scenes/global/Topbar"; import Sidebar from "./scenes/global/Sidebar"; import Beranda from "./scenes/beranda"; import SiswaTerdaftar from "./scenes/siswaTerdaftar"; function App() { const [theme, colorMode] = useMode(); return ( <ColorModeContext.Provider value={colorMode}> <ThemeProvider theme={theme}> <CssBaseline /> <Sidebar /> <div style={{ height: "100%", width: "100%" }}> <Topbar /> <main className="content"> <Routes> <Route path="/" element={<Beranda />} /> <Route path="/siswaterdaftar" element={<SiswaTerdaftar />} /> </Routes> </main> </div> </ThemeProvider> </ColorModeContext.Provider> ); } export default App;
Sidebar组件代码
import * as React from "react"; import { useState } from "react"; import { ProSidebar, Menu, MenuItem, SidebarFooter, SidebarContent, SidebarHeader, } from "react-pro-sidebar"; import { Box, IconButton, Button, Typography, useTheme } from "@mui/material"; import { Link } from "react-router-dom"; import "react-pro-sidebar/dist/css/styles.css"; import { tokens } from "../../theme"; import MenuRoundedIcon from "@mui/icons-material/MenuRounded"; import HomeRoundedIcon from "@mui/icons-material/HomeRounded"; import InventoryRoundedIcon from "@mui/icons-material/InventoryRounded"; import HowToRegRoundedIcon from "@mui/icons-material/HowToRegRounded"; import logoSMK from "../../assets/images/smk-informatika.png"; import BasicDialogs from "../../components/common/Modal/dialog"; import DaftarUlangForm from "../modal/DaftarUlangForm"; function Copyright(props) { return ( <Typography variant="body2" color="text.secondary" align="center" {...props} > {"Copyright © "} <Link color="inherit" href="https://mui.com/"> Tentackle </Link>{" "} {new Date().getFullYear()} {"."} </Typography> ); } const Item = ({ title, to, icon, selected, setSelected }) => { const theme = useTheme(); const colors = tokens(theme.palette.mode); return ( <MenuItem active={selected === title} style={{ color: colors.grey[200], }} onClick={() => setSelected(title)} icon={icon} > <Typography fontWeight="medium">{title}</Typography> <Link to={to} /> </MenuItem> ); }; const Sidebar = () => { const theme = useTheme(); const colors = tokens(theme.palette.mode); const [isCollapsed, setIsCollapsed] = useState(false); const [selected, setSelected] = useState("Beranda"); const [open, setOpen] = React.useState(false); return ( <Box sx={{ position: "sticky", height: "100vh", top: 0, bottom: 0, "& .pro-sidebar-inner": { background: `${colors.primary[400]} !important`, }, "& .pro-icon-wrapper": { backgroundColor: "transparent !important", }, "& .pro-inner-item": { padding: "5px 35px 5px 20px !important", }, "& .pro-inner-item:hover": { color: "#4389a9 !important", }, "& .pro-menu-item.active": { color: "#4389a9 !important", }, }} > <ProSidebar breakPoint="md" collapsed={isCollapsed}> <SidebarHeader> <Menu iconShape="square"> {/* LOGO AND MENU ICON */} <MenuItem onClick={() => setIsCollapsed(!isCollapsed)} icon={isCollapsed ? <MenuRoundedIcon /> : undefined} style={{ margin: "10px 0 0 0", color: colors.grey[100], }} > {!isCollapsed && ( <Box display="flex" justifyContent="space-between" alignItems="center" ml="20px" > <Box display="flex" justifyContent="center" alignItems="center" > <img alt="profile-user" width="30px" height="30px" src={logoSMK} style={{ cursor: "pointer", borderRadius: "50%" }} /> </Box> <Typography variant="h4" fontWeight="bold" color={colors.blueAccent[400]} > Admin PPDB </Typography> <IconButton onClick={() => setIsCollapsed(!isCollapsed)}> <MenuRoundedIcon /> </IconButton> </Box> )} </MenuItem> </Menu> </SidebarHeader> <SidebarContent> <Menu> <Box paddingLeft={isCollapsed ? undefined : "10%"}> <Item title="Beranda" to="/" fontWeight="bold" icon={<HomeRoundedIcon />} selected={selected} setSelected={setSelected} /> <Item title="Siswa Terdaftar" to="/siswaTerdaftar" fontWeight="bold" icon={<InventoryRoundedIcon />} selected={selected} setSelected={setSelected} /> </Box> </Menu> </SidebarContent> <SidebarFooter> <Box sx={{ mt: 2, mb: 2, ml:1 }} paddingLeft={isCollapsed ? undefined : "16%"} > <BasicDialogs title="Formulir Daftar Ulang" setOpen={setOpen} open={open} > <DaftarUlangForm /> </BasicDialogs> <Button onClick={() => setOpen(true)} sx={{ backgroundColor: colors.blueAccent[700], color: colors.grey[100], fontSize: "14px", fontWeight: "bold", padding: "10px 20px", }} > <HowToRegRoundedIcon sx={{ mr: "3px" }} /> {!isCollapsed && "Daftar Ulang!"} </Button> </Box> <Copyright sx={{ mt: 2, mb: 2 }} /> </SidebarFooter> </ProSidebar> </Box> ); }; export default Sidebar;
问题原因分析
- 路由路径大小写不匹配:App组件中路由路径为
/siswaterdaftar,但Sidebar中Item的to属性是/siswaTerdaftar,大小写差异会导致第一次点击时路由无法匹配,看起来未跳转。 Link与MenuItem事件冲突:Item组件中把空的Link放在MenuItem内部,同时MenuItem绑定了onClick仅更新选中状态,第一次点击只会修改selected状态,不会触发路由跳转;第二次点击才会触发Link的跳转逻辑,导致需要双击。
解决方案
方案一:统一路径并同步点击事件
- 修复路由路径:将App组件中的路由路径改为和Sidebar一致的
/siswaTerdaftar:
<Route path="/siswaTerdaftar" element={<SiswaTerdaftar />} />
- 修改
Item组件:在MenuItem的点击事件中同时触发路由跳转,移除空的Link标签:
import { useNavigate } from "react-router-dom"; const Item = ({ title, to, icon, selected, setSelected }) => { const theme = useTheme(); const colors = tokens(theme.palette.mode); const navigate = useNavigate(); const handleClick = () => { setSelected(title); navigate(to); }; return ( <MenuItem active={selected === title} style={{ color: colors.grey[200] }} onClick={handleClick} icon={icon} > <Typography fontWeight="medium">{title}</Typography> </MenuItem> ); };
方案二:基于路由自动同步选中状态(更推荐)
这种方式让选中状态完全由当前路由决定,避免手动维护状态的不一致:
- 修改Sidebar组件:使用
useLocation和useMemo根据当前路由自动计算选中项:
import { useLocation, useMemo } from "react-router-dom"; const Sidebar = () => { const theme = useTheme(); const colors = tokens(theme.palette.mode); const [isCollapsed, setIsCollapsed] = useState(false); const [open, setOpen] = React.useState(false); const location = useLocation(); // 根据当前路由确定选中的菜单项 const selected = useMemo(() => { switch (location.pathname) { case "/": return "Beranda"; case "/siswaTerdaftar": return "Siswa Terdaftar"; default: return "Beranda"; } }, [location.pathname]); // 其余代码保持不变 };
- 更新
Item组件:移除setSelected参数,让Link包裹菜单项内容,由Link处理路由跳转:
const Item = ({ title, to, icon, selected }) => { const theme = useTheme(); const colors = tokens(theme.palette.mode); return ( <MenuItem active={selected === title} style={{ color: colors.grey[200] }} icon={icon} > <Link to={to}> <Typography fontWeight="medium">{title}</Typography> </Link> </MenuItem> ); };
- 同步修改Sidebar中
Item的调用:去掉setSelected属性:
<Item title="Beranda" to="/" fontWeight="bold" icon={<HomeRoundedIcon />} selected={selected} /> <Item title="Siswa Terdaftar" to="/siswaTerdaftar" fontWeight="bold" icon={<InventoryRoundedIcon />} selected={selected} />
这样修改后,点击菜单项时Link会直接触发路由跳转,同时选中状态会根据当前路由自动更新,彻底解决双击问题。
内容的提问来源于stack exchange,提问作者Syafiqu Rohman
相关产品推荐
相关产品推荐

