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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 20:47:49