React Router 路由
大约 9 分钟约 2559 字
React Router 路由
简介
React Router 是 React 应用的标准路由库,管理页面间的导航和 URL 映射。React Router v6 采用声明式路由设计,API 更简洁,支持嵌套路由、布局路由和数据加载。
特点
基本配置
路由定义
// npm install react-router-dom
import { createBrowserRouter, RouterProvider, Outlet } from 'react-router-dom'
import { lazy, Suspense } from 'react'
// 懒加载
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Users = lazy(() => import('./pages/Users'))
const UserDetail = lazy(() => import('./pages/UserDetail'))
const Login = lazy(() => import('./pages/Login'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
// 布局组件
function Layout() {
return (
<div>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/users">用户</Link>
</nav>
<Suspense fallback={<div>加载中...</div>}>
<Outlet /> {/* 子路由渲染位置 */}
</Suspense>
</div>
)
}
// 路由配置
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
{ path: 'users', element: <Users /> },
{ path: 'users/:id', element: <UserDetail /> },
]
},
{ path: '/login', element: <Login /> },
{
path: '/dashboard',
element: <Dashboard />,
loader: async () => {
const token = localStorage.getItem('token')
if (!token) throw redirect('/login')
return { authenticated: true }
}
}
])
function App() {
return <RouterProvider router={router} />
}编程式导航
useNavigate
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
function UserDetail() {
const { id } = useParams() // 路由参数
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const tab = searchParams.get('tab') || 'info'
const goBack = () => navigate(-1)
const goHome = () => navigate('/', { replace: true })
const switchTab = (tab: string) => {
navigate(`/users/${id}?tab=${tab}`)
}
return (
<div>
<h1>用户 {id}</h1>
<button onClick={() => switchTab('info')}>信息</button>
<button onClick={() => switchTab('orders')}>订单</button>
<button onClick={goBack}>返回</button>
</div>
)
}数据加载
Loader 和 Action
import { createBrowserRouter, useLoaderData, Form, useActionData } from 'react-router-dom'
// Loader:进入页面前加载数据
async function usersLoader() {
const response = await fetch('/api/users')
if (!response.ok) throw new Error('加载失败')
return response.json()
}
// Action:处理表单提交
async function createUserAction({ request }) {
const formData = await request.formData()
const data = Object.fromEntries(formData)
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (!response.ok) {
return { error: '创建失败' }
}
return redirect('/users')
}
// 路由配置
const router = createBrowserRouter([
{
path: '/users',
element: <UsersPage />,
loader: usersLoader,
action: createUserAction,
}
])
// 使用 Loader 数据
function UsersPage() {
const users = useLoaderData() as User[]
const actionData = useActionData()
return (
<div>
{actionData?.error && <p className="error">{actionData.error}</p>}
<Form method="post">
<input name="name" placeholder="用户名" required />
<input name="email" type="email" placeholder="邮箱" required />
<button type="submit">创建</button>
</Form>
<ul>
{users.map(user => (
<li key={user.id}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
</div>
)
}路由守卫
权限控制
import { Navigate, useLocation } from 'react-router-dom'
function RequireAuth({ children }: { children: React.ReactNode }) {
const token = localStorage.getItem('token')
const location = useLocation()
if (!token) {
return <Navigate to="/login" state={{ from: location }} replace />
}
return <>{children}</>
}
// 使用
const router = createBrowserRouter([
{
path: '/admin',
element: (
<RequireAuth>
<AdminLayout />
</RequireAuth>
),
children: [
{ index: true, element: <Dashboard /> },
{ path: 'settings', element: <Settings /> },
]
}
])
// 登录后跳转回原页面
function LoginPage() {
const navigate = useNavigate()
const location = useLocation()
const from = (location.state as any)?.from?.pathname || '/'
const handleLogin = async () => {
// ... 登录逻辑
navigate(from, { replace: true })
}
}嵌套路由高级模式
// 多级嵌套布局
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{ index: true, element: <Home /> },
{
path: 'admin',
element: <RequireAuth><AdminLayout /></RequireAuth>,
children: [
{ index: true, element: <Navigate to="dashboard" replace /> },
{
path: 'dashboard',
element: <Dashboard />,
handle: { title: '仪表盘', breadcrumb: ['首页', '管理后台', '仪表盘'] }
},
{
path: 'users',
element: <Users />,
loader: usersLoader,
},
{
path: 'users/:userId',
element: <UserDetail />,
loader: userDetailLoader,
children: [
{ index: true, element: <UserInfo /> },
{ path: 'orders', element: <UserOrders /> },
{ path: 'settings', element: <UserSettings /> },
]
},
]
}
]
},
])
// RootLayout — 带导航栏的全局布局
function RootLayout() {
return (
<div className="app">
<Header />
<main>
<Suspense fallback={<PageLoading />}>
<Outlet />
</Suspense>
</main>
</div>
)
}
// AdminLayout — 带侧边栏的管理后台布局
function AdminLayout() {
return (
<div className="admin-layout">
<Sidebar />
<div className="admin-content">
<Breadcrumb />
<Outlet />
</div>
</div>
)
}面包屑导航
// components/Breadcrumb.tsx
import { Link, useMatches } from 'react-router-dom'
function Breadcrumb() {
const matches = useMatches()
// matches 包含所有匹配的路由记录
const crumbs = matches
.filter(match => match.handle?.breadcrumb)
.map(match => ({
path: match.pathname,
label: match.handle.breadcrumb[match.handle.breadcrumb.length - 1]
}))
return (
<nav className="breadcrumb">
<Link to="/">首页</Link>
{crumbs.map((crumb, index) => (
<span key={crumb.path}>
<span className="separator">/</span>
{index === crumbs.length - 1 ? (
<span className="active">{crumb.label}</span>
) : (
<Link to={crumb.path}>{crumb.label}</Link>
)}
</span>
))}
</nav>
)
}useSearchParams 高级用法
import { useSearchParams } from 'react-router-dom'
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams()
// 读取查询参数(带类型转换和默认值)
const page = Number(searchParams.get('page')) || 1
const pageSize = Number(searchParams.get('pageSize')) || 20
const keyword = searchParams.get('keyword') || ''
const sortBy = searchParams.get('sortBy') || 'createdAt'
const sortOrder = searchParams.get('sortOrder') || 'desc'
// 批量更新查询参数
function updateFilters(newFilters: Record<string, string>) {
setSearchParams(prev => {
const next = new URLSearchParams(prev)
Object.entries(newFilters).forEach(([key, value]) => {
if (value) {
next.set(key, value)
} else {
next.delete(key)
}
})
// 重置到第一页
next.set('page', '1')
return next
}, { replace: true })
}
function handleSearch(keyword: string) {
updateFilters({ keyword, page: '1' })
}
function handleSort(field: string) {
const newOrder = sortBy === field && sortOrder === 'asc' ? 'desc' : 'asc'
setSearchParams(prev => {
prev.set('sortBy', field)
prev.set('sortOrder', newOrder)
return prev
})
}
function resetFilters() {
setSearchParams({})
}
}404 和错误处理
错误边界
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
function ErrorBoundary() {
const error = useRouteError()
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data?.message || '页面未找到'}</p>
<Link to="/">返回首页</Link>
</div>
)
}
return (
<div>
<h1>出错了</h1>
<p>{(error as Error).message}</p>
</div>
)
}
// 路由配置
const router = createBrowserRouter([
// ... 其他路由
{ path: '*', element: <Navigate to="/404" replace /> },
{ path: '/404', element: <NotFound /> }
], {
basename: '/app' // 基础路径
})延迟加载与 Deferred 数据
import { defer, Await, useLoaderData } from 'react-router-dom'
// defer — 关键数据先返回,非关键数据延迟加载
function dashboardLoader() {
const criticalData = fetch('/api/dashboard/stats').then(r => r.json())
const slowData = fetch('/api/dashboard/analytics').then(r => r.json())
return defer({
stats: criticalData, // 先显示
analytics: slowData, // 后显示
})
}
// Dashboard 组件
function Dashboard() {
const data = useLoaderData() as {
stats: Promise<StatsData>
analytics: Promise<AnalyticsData>
}
return (
<div>
{/* 关键数据 — 立即渲染 */}
<React.Suspense fallback={<StatsSkeleton />}>
<Await resolve={data.stats}>
{(stats) => <StatsCards data={stats} />}
</Await>
</React.Suspense>
{/* 非关键数据 — 延迟渲染 */}
<React.Suspense fallback={<AnalyticsSkeleton />}>
<Await resolve={data.analytics}>
{(analytics) => <AnalyticsChart data={analytics} />}
</Await>
</React.Suspense>
</div>
)
}角色权限控制
// 高级权限控制 — 支持多角色
function RequireRole({ children, roles }: {
children: React.ReactNode
roles: string[]
}) {
const { user, roles: userRoles } = useContext(AuthContext)
const location = useLocation()
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />
}
const hasRole = roles.some(role => userRoles.includes(role))
if (!hasRole) {
return (
<div className="forbidden">
<h1>403 - 无权限访问</h1>
<p>您没有访问此页面的权限。</p>
<Link to="/">返回首页</Link>
</div>
)
}
return <>{children}</>
}
// 使用
{
path: 'admin',
element: (
<RequireRole roles={['admin', 'superadmin']}>
<AdminPanel />
</RequireRole>
),
}路由滚动恢复
import { createBrowserRouter } from 'react-router-dom'
const router = createBrowserRouter(routes, {
scrollBehavior({ offset }) {
window.scrollTo(0, 0)
}
})
// 精细控制滚动位置
function ScrollToTop() {
const { pathname } = useLocation()
useEffect(() => {
window.scrollTo(0, 0)
}, [pathname])
return null
}
// 在 RootLayout 中使用
function RootLayout() {
return (
<>
<ScrollToTop />
<Header />
<Outlet />
</>
)
}useSearchParams 高级用法
import { useSearchParams } from 'react-router-dom'
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page')) || 1
const pageSize = Number(searchParams.get('pageSize')) || 20
const keyword = searchParams.get('keyword') || ''
const sortBy = searchParams.get('sortBy') || 'createdAt'
// 批量更新查询参数
function updateFilters(newFilters: Record<string, string>) {
setSearchParams(prev => {
const next = new URLSearchParams(prev)
Object.entries(newFilters).forEach(([key, value]) => {
if (value) next.set(key, value)
else next.delete(key)
})
next.set('page', '1') // 重置到第一页
return next
}, { replace: true })
}
function handleSort(field: string) {
const currentOrder = searchParams.get('sortOrder') || 'desc'
const newOrder = currentOrder === 'asc' ? 'desc' : 'asc'
setSearchParams(prev => {
prev.set('sortBy', field)
prev.set('sortOrder', newOrder)
return prev
})
}
function resetFilters() {
setSearchParams({})
}
}路由模块化配置
// routes/index.tsx — 模块化路由配置
import { createBrowserRouter } from 'react-router-dom'
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{
index: true,
lazy: () => import('./routes/Home').then(m => ({
Component: m.default,
loader: m.loader,
}))
},
{
path: 'users',
lazy: () => import('./routes/Users').then(m => ({
Component: m.default,
loader: m.loader,
}))
},
{
path: 'admin',
element: <RequireAuth><AdminLayout /></RequireAuth>,
children: [
{ index: true, element: <Navigate to="dashboard" replace /> },
{
path: 'dashboard',
lazy: () => import('./routes/admin/Dashboard'),
},
]
},
]
},
{
path: '/login',
lazy: () => import('./routes/Login'),
},
{
path: '*',
lazy: () => import('./routes/NotFound'),
},
])优点
缺点
总结
React Router v6 核心:createBrowserRouter 声明式配置路由、Outlet 渲染子路由、useNavigate 编程式导航、useParams 获取路由参数。Loader 在进入页面前加载数据,Action 处理表单提交。路由守卫用高阶组件包裹需要认证的路由。懒加载用 lazy() + Suspense 代码分割。错误处理用 errorElement 和 useRouteError。
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《React Router 路由》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《React Router 路由》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《React Router 路由》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《React Router 路由》最大的收益和代价分别是什么?
