React Router Dom 6.4侧边栏下拉菜单Key错误排查与解决
React Router Dom 6.4 层级侧边栏(文件夹>子文件夹>任务)最优实现
问题原因分析
你遇到的Key错误,核心问题不是Key生成方式,而是在map中嵌套条件渲染时,元素的Key对应关系不稳定。比如在map里用if/else过滤元素,可能导致同一份数据在不同渲染周期里的Key位置变化,或重复使用Key;而用Nanoid的话,每次渲染都会生成新Key,反而触发不必要的组件卸载/重挂载,既影响性能也可能引发错误。改成先filter再map,相当于先把要渲染的数据集固定下来,再给每个元素分配稳定的唯一Key(比如数据自身的id),自然解决了Key冲突问题。
最优实现示例
下面是基于函数组件+RRD6.4核心API(useRoutes、Outlet、useParams)的完整实现,包含侧边栏折叠/展开、层级路由匹配、稳定Key处理:
1. 定义路由数据结构
先准备符合层级关系的路由/菜单数据,每个节点要有唯一id、path、children(子节点)、label:
// src/data/menuData.js export const menuData = [ { id: 'folder-1', label: '项目文件夹', path: '/folders/folder-1', children: [ { id: 'subfolder-1-1', label: '前端模块', path: '/folders/folder-1/subfolders/subfolder-1-1', children: [ { id: 'task-1-1-1', label: '重构组件', path: '/folders/folder-1/subfolders/subfolder-1-1/tasks/task-1-1-1' }, { id: 'task-1-1-2', label: '优化性能', path: '/folders/folder-1/subfolders/subfolder-1-1/tasks/task-1-1-2' } ] }, { id: 'subfolder-1-2', label: '后端模块', path: '/folders/folder-1/subfolders/subfolder-1-2', children: [ { id: 'task-1-2-1', label: '接口调试', path: '/folders/folder-1/subfolders/subfolder-1-2/tasks/task-1-2-1' } ] } ] }, { id: 'folder-2', label: '个人文件夹', path: '/folders/folder-2', children: [ { id: 'task-2-1', label: '整理笔记', path: '/folders/folder-2/tasks/task-2-1' } ] } ];
2. 侧边栏菜单组件(递归渲染层级)
用递归组件渲染多级菜单,始终用数据自身的id作为Key,避免动态生成Key:
// src/components/Sidebar.js import { Link, useLocation } from 'react-router-dom'; import { useState } from 'react'; const SidebarItem = ({ item }) => { const [isExpanded, setIsExpanded] = useState(false); const location = useLocation(); const isActive = location.pathname.startsWith(item.path); const hasChildren = item.children?.length > 0; return ( <div className="sidebar-item"> <div className={`item-header ${isActive ? 'active' : ''}`} onClick={() => hasChildren && setIsExpanded(!isExpanded)} > <Link to={item.path} className="item-link"> {item.label} </Link> {hasChildren && ( <span className="expand-icon">{isExpanded ? '▼' : '▶'}</span> )} </div> {/* 先过滤有效子节点再渲染,避免map里的条件判断 */} {hasChildren && isExpanded && ( <div className="sidebar-submenu"> {item.children .filter(child => child.path) .map(child => ( <SidebarItem key={child.id} item={child} /> ))} </div> )} </div> ); }; const Sidebar = ({ menuData }) => { return ( <div className="sidebar"> {menuData .filter(item => item.path) .map(item => ( <SidebarItem key={item.id} item={item} /> ))} </div> ); }; export default Sidebar;
3. 层级路由配置与Outlet使用
用useRoutes定义嵌套路由,配合Outlet渲染子组件,确保路由层级和菜单结构一致:
// src/App.js import { useRoutes, Outlet } from 'react-router-dom'; import Sidebar from './components/Sidebar'; import { menuData } from './data/menuData'; // 页面组件示例 const FolderPage = () => <div>当前文件夹页面</div>; const SubfolderPage = () => <div>当前子文件夹页面</div>; const TaskPage = () => <div>当前任务详情页面</div>; // 递归生成路由配置 const generateRoutes = (items) => { return items.map(item => { const children = item.children ? generateRoutes(item.children) : []; let element; if (item.path.includes('/tasks/')) { element = <TaskPage />; } else if (item.path.includes('/subfolders/')) { element = <SubfolderPage />; } else { element = <FolderPage />; } return { path: item.path, element: element, children: children.length > 0 ? [{ path: '', element: <Outlet /> }] : [] }; }); }; const App = () => { const routes = useRoutes([ { path: '/', element: ( <div className="app-container"> <Sidebar menuData={menuData} /> <div className="content"> <Outlet /> </div> </div> ), children: generateRoutes(menuData) }, { path: '*', element: <div>404 Not Found</div> } ]); return routes; }; export default App;
4. 核心注意事项
- Key的稳定性:永远用数据的唯一标识(如
id)作为Key,不要用索引、Nanoid这类动态生成的值,避免组件不必要的重渲染。 - 先filter再map:提前过滤掉不需要渲染的节点(比如无路径的无效节点),再进行map渲染,避免在map中做条件判断导致Key对应关系混乱。
- 路由与菜单数据同步:菜单数据和路由配置共用一份数据源,避免两者不一致导致的匹配错误。
关键钩子与API说明
useRoutes:RRD6.4推荐的路由配置方式,比<Routes>更灵活,适合动态生成嵌套路由。Outlet:用于渲染父路由的子路由组件,是实现层级页面嵌套的核心。useParams:如果需要获取当前路由的参数(比如文件夹id、任务id),可以在页面组件中使用:import { useParams } from 'react-router-dom'; const TaskPage = () => { const { taskId } = useParams(); // 需在路由配置中定义path为`/tasks/:taskId` return <div>任务ID:{taskId}</div>; };useLocation:用于判断当前路由是否匹配菜单项,实现激活态高亮。
内容的提问来源于stack exchange,提问作者Dump
相关产品推荐
相关产品推荐

