学完这篇,你能从零搭出一套 React Router v6 的路由骨架,包括页面切换、嵌套路由、动态参数和 404 兜底,并且知道哪些写法在 v6 里已经失效。文中代码基于 react-router-dom v6(示例用 v6.22 验证过),配 Vite + React 18 环境。
第一步:装依赖、包住根组件
npm i react-router-dom
路由必须在最外层包一层 BrowserRouter,否则组件里的 Link、useNavigate 会直接报错:
// src/main.jsx
import { BrowserRouter } from 'react-router-dom'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
)
注意:BrowserRouter 只包一次。包在 App 里面会导致 React 报错,包两次也可能出现多层嵌套;如果用的是 createBrowserRouter 方案(见第五步),就不要再包 BrowserRouter。
第二步:Routes 和 Route 的最小写法
v5 的 <Switch> 已经删除,统一用 <Routes>;component={Home} 改成 element={<Home />},必须传 JSX 元素,直接传组件引用不会渲染。
// src/App.jsx
import { Routes, Route } from 'react-router-dom'
export default function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
)
}
注意:v6 不再需要 exact,也不需要你关心顺序,它按“最优匹配”打分;同时路径里不再支持正则表达式,path="/post/:id(\d+)" 这种写法在 v6 会直接抛异常,参数校验放组件里做。
第三步:用嵌套路由 + Outlet 搭公共布局
嵌套路由是 v6 最核心的变化:父路由只负责渲染公共部分,子路由内容通过 <Outlet /> 渲染出来。
// src/Layout.jsx
import { Outlet, NavLink } from 'react-router-dom'
export default function Layout() {
return (
<div>
<nav>
<NavLink to="/">首页</NavLink>
<NavLink to="/posts">帖子</NavLink>
</nav>
<main><Outlet /></main>
</div>
)
}
// src/App.jsx
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="posts" element={<PostList />} />
<Route path="posts/:id" element={<PostDetail />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
这里的 index 等价于“父路径 / 的默认子页”。
注意:子路由的 path 不要写前导斜杠。path="/posts" 写在嵌套里会被当成绝对路径,父布局失效;正确写法是 path="posts"。另一个高频问题:父路由 element 里忘了写 <Outlet />,页面一片空白,控制台却没有报错。
第四步:取参数、跳转与 404
动态段用 :id 声明,组件里用 useParams 取:
import { useParams, useNavigate } from 'react-router-dom'
export default function PostDetail() {
const { id } = useParams()
const navigate = useNavigate()
return (
<>
<p>当前帖子 ID:{id}</p>
<button onClick={() => navigate(-1)}>返回</button>
</>
)
}
useNavigate() 替代了 v5 的 useHistory(),history.push('/x') 要改成 navigate('/x')。
兜底路由写 path="*",放在 <Routes> 内任意位置都行,v6 会自动优先匹配具体路径。
第五步:进阶——数据路由(v6.4+)
如果你的页面需要“进入前先拿数据”,用 createBrowserRouter 的 loader 更省事,避免 useEffect 里套 useState 的加载状态。
import { createBrowserRouter, RouterProvider, useLoaderData } from 'react-router-dom'
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'posts', element: <PostList /> },
{
path: 'posts/:id',
element: <PostDetail />,
loader: ({ params }) => fetch(`/api/posts/${params.id}`).then(r => r.json()),
},
],
},
{ path: '*', element: <NotFound /> },
])
createRoot(document.getElementById('root')).render(<RouterProvider router={router} />)
组件里用 const post = useLoaderData() 直接拿结果。换成这套写法后,要删掉第一步的 BrowserRouter,否则路由上下文冲突。
小结
- 装
react-router-dom,BrowserRouter 只包一层根组件。
<Switch> → <Routes>,component={} → element={<>},不再需要 exact。
- 嵌套路由靠
<Outlet /> 渲染子级,子路径不写前导斜杠,index 表示默认子页。
- 动态参数
:id + useParams(),跳转用 useNavigate(),404 用 path="*"。
- 需要进门先取数据,改用
createBrowserRouter + loader + useLoaderData。
- 最常踩的三个坑:漏写
Outlet、子路由写了绝对路径、v6 里还试图用正则路径。