跳到主要内容

React Router 原理与使用

问题

React Router 的工作原理是什么?如何在 React 应用中实现路由功能?

答案

React Router 是 React 生态中最流行的路由库,用于在单页应用(SPA)中实现客户端路由。

核心概念


路由模式

BrowserRouter(推荐)

使用 HTML5 History API

import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserDetail />} />
</Routes>
</BrowserRouter>
);
}

// URL 示例: https://example.com/about
// 需要服务器配置支持(所有路径返回 index.html)

HashRouter

使用 URL hash:

import { HashRouter, Routes, Route } from 'react-router-dom';

function App() {
return (
<HashRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</HashRouter>
);
}

// URL 示例: https://example.com/#/about
// 无需服务器配置,但 SEO 不友好

两种模式对比

特性BrowserRouterHashRouter
URL 格式/path/#/path
实现原理History APIhashchange 事件
服务器配置需要配置无需配置
SEO友好不友好
兼容性IE10+IE8+
推荐场景生产环境静态托管、兼容需求

基本用法

路由配置

import { 
createBrowserRouter,
RouterProvider,
Route,
createRoutesFromElements
} from 'react-router-dom';

// 方式1: 对象配置(推荐)
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
{ path: 'users', element: <Users />,
children: [
{ path: ':userId', element: <UserDetail /> }
]
},
{ path: '*', element: <NotFound /> }
]
}
]);

// 方式2: JSX 配置
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Layout />} errorElement={<ErrorPage />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users" element={<Users />}>
<Route path=":userId" element={<UserDetail />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
)
);

function App() {
return <RouterProvider router={router} />;
}

嵌套路由与 Outlet

// Layout.tsx - 父路由组件
import { Outlet, Link } from 'react-router-dom';

function Layout() {
return (
<div>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/users">用户</Link>
</nav>

<main>
{/* 子路由渲染位置 */}
<Outlet />
</main>

<footer>Footer</footer>
</div>
);
}

// Users.tsx - 也可以有 Outlet
function Users() {
return (
<div>
<h1>用户列表</h1>
<UserList />
{/* 嵌套子路由 /users/:userId 渲染位置 */}
<Outlet />
</div>
);
}
import { Link, NavLink } from 'react-router-dom';

function Navigation() {
return (
<nav>
{/* 基础链接 */}
<Link to="/about">关于</Link>

{/* 带状态的链接 */}
<Link to="/users" state={{ from: 'nav' }}>
用户
</Link>

{/* NavLink: 自动添加 active 类 */}
<NavLink
to="/dashboard"
className={({ isActive, isPending }) =>
isActive ? 'active' : isPending ? 'pending' : ''
}
>
控制台
</NavLink>

{/* NavLink: 自定义样式 */}
<NavLink
to="/settings"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'red' : 'black'
})}
>
设置
</NavLink>
</nav>
);
}

Hooks API

useParams

获取动态路由参数:

import { useParams } from 'react-router-dom';

// 路由: /users/:userId/posts/:postId
function PostDetail() {
const { userId, postId } = useParams<{
userId: string;
postId: string;
}>();

return (
<div>
<p>用户 ID: {userId}</p>
<p>文章 ID: {postId}</p>
</div>
);
}

useNavigate

编程式导航:

import { useNavigate } from 'react-router-dom';

function LoginForm() {
const navigate = useNavigate();

async function handleSubmit(e: React.FormEvent) {
e.preventDefault();

const success = await login(credentials);
if (success) {
// 导航到首页
navigate('/');

// 替换当前历史记录
navigate('/dashboard', { replace: true });

// 携带状态
navigate('/profile', { state: { from: 'login' } });

// 后退
navigate(-1);

// 前进
navigate(1);
}
}

return <form onSubmit={handleSubmit}>...</form>;
}

useLocation

获取当前位置信息:

import { useLocation } from 'react-router-dom';

function CurrentPage() {
const location = useLocation();

// location 对象结构
// {
// pathname: "/users/123",
// search: "?tab=posts",
// hash: "#section1",
// state: { from: "nav" },
// key: "default"
// }

return (
<div>
<p>路径: {location.pathname}</p>
<p>查询: {location.search}</p>
<p>哈希: {location.hash}</p>
<p>状态: {JSON.stringify(location.state)}</p>
</div>
);
}

useSearchParams

管理 URL 查询参数:

import { useSearchParams } from 'react-router-dom';

function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();

// 获取参数 /search?q=react&page=2
const query = searchParams.get('q'); // "react"
const page = searchParams.get('page'); // "2"

function handleSearch(term: string) {
// 设置参数
setSearchParams({ q: term, page: '1' });
}

function nextPage() {
// 更新参数
setSearchParams(prev => {
prev.set('page', String(Number(prev.get('page') || 1) + 1));
return prev;
});
}

return (
<div>
<input
value={query || ''}
onChange={e => handleSearch(e.target.value)}
/>
<button onClick={nextPage}>下一页</button>
</div>
);
}

useMatch

匹配路由模式:

import { useMatch } from 'react-router-dom';

function UserAvatar() {
// 检查是否匹配特定路由
const match = useMatch('/users/:userId');

if (match) {
const { userId } = match.params;
return <Avatar userId={userId} />;
}

return <DefaultAvatar />;
}

数据加载(v6.4+)

loader 函数

路由渲染前加载数据:

import { 
createBrowserRouter,
useLoaderData,
defer,
Await
} from 'react-router-dom';
import { Suspense } from 'react';

// 定义 loader
async function userLoader({ params }: { params: { userId: string } }) {
const response = await fetch(`/api/users/${params.userId}`);
if (!response.ok) {
throw new Response('User not found', { status: 404 });
}
return response.json();
}

// 路由配置
const router = createBrowserRouter([
{
path: '/users/:userId',
element: <UserDetail />,
loader: userLoader,
errorElement: <UserError />
}
]);

// 组件中使用数据
function UserDetail() {
const user = useLoaderData() as User;

return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}

// 延迟加载(流式渲染)
async function dashboardLoader() {
return defer({
user: getCurrentUser(), // 立即加载
posts: fetchPosts(), // 延迟加载
comments: fetchComments() // 延迟加载
});
}

function Dashboard() {
const { user, posts, comments } = useLoaderData() as {
user: User;
posts: Promise<Post[]>;
comments: Promise<Comment[]>;
};

return (
<div>
<h1>欢迎, {user.name}</h1>

<Suspense fallback={<p>加载文章...</p>}>
<Await resolve={posts}>
{(resolvedPosts) => <PostList posts={resolvedPosts} />}
</Await>
</Suspense>
</div>
);
}

action 函数

处理表单提交:

import { 
Form,
useActionData,
useNavigation,
redirect
} from 'react-router-dom';

// 定义 action
async function createUserAction({ request }: { request: Request }) {
const formData = await request.formData();
const name = formData.get('name');
const email = formData.get('email');

// 验证
const errors: Record<string, string> = {};
if (!name) errors.name = '姓名必填';
if (!email) errors.email = '邮箱必填';

if (Object.keys(errors).length) {
return { errors };
}

// 创建用户
await createUser({ name, email });

// 重定向
return redirect('/users');
}

// 路由配置
const router = createBrowserRouter([
{
path: '/users/new',
element: <CreateUser />,
action: createUserAction
}
]);

// 组件
function CreateUser() {
const actionData = useActionData() as { errors?: Record<string, string> };
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';

return (
<Form method="post">
<div>
<label>姓名</label>
<input name="name" />
{actionData?.errors?.name && (
<span className="error">{actionData.errors.name}</span>
)}
</div>

<div>
<label>邮箱</label>
<input name="email" type="email" />
{actionData?.errors?.email && (
<span className="error">{actionData.errors.email}</span>
)}
</div>

<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '创建用户'}
</button>
</Form>
);
}

路由守卫

认证守卫

import { Navigate, useLocation, Outlet } from 'react-router-dom';

interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
}

function ProtectedRoute() {
const { isAuthenticated } = useAuth();
const location = useLocation();

if (!isAuthenticated) {
// 重定向到登录页,保存当前位置
return <Navigate to="/login" state={{ from: location }} replace />;
}

return <Outlet />;
}

// 路由配置
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: 'login', element: <Login /> },

// 受保护的路由
{
element: <ProtectedRoute />,
children: [
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'profile', element: <Profile /> },
{ path: 'settings', element: <Settings /> }
]
}
]
}
]);

// 登录后重定向回原页面
function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = (location.state as { from?: Location })?.from?.pathname || '/';

async function handleLogin() {
await login();
navigate(from, { replace: true });
}

return <button onClick={handleLogin}>登录</button>;
}

权限守卫

interface PermissionGuardProps {
permissions: string[];
children: React.ReactNode;
fallback?: React.ReactNode;
}

function PermissionGuard({
permissions,
children,
fallback = <Forbidden />
}: PermissionGuardProps) {
const { user } = useAuth();

const hasPermission = permissions.every(
p => user?.permissions.includes(p)
);

if (!hasPermission) {
return fallback;
}

return <>{children}</>;
}

// 使用
function AdminPage() {
return (
<PermissionGuard permissions={['admin:read', 'admin:write']}>
<AdminDashboard />
</PermissionGuard>
);
}

懒加载路由

import { lazy, Suspense } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

// 懒加载组件
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const UserList = lazy(() => import('./pages/UserList'));

// 加载组件
function PageLoader() {
return <div className="page-loader">加载中...</div>;
}

// 包装懒加载组件
function lazyLoad(Component: React.LazyExoticComponent<() => JSX.Element>) {
return (
<Suspense fallback={<PageLoader />}>
<Component />
</Suspense>
);
}

const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: 'dashboard', element: lazyLoad(Dashboard) },
{ path: 'settings', element: lazyLoad(Settings) },
{ path: 'users', element: lazyLoad(UserList) }
]
}
]);

// 或者使用 route.lazy(v6.4+)
const router = createBrowserRouter([
{
path: '/dashboard',
lazy: async () => {
const { Dashboard } = await import('./pages/Dashboard');
return { Component: Dashboard };
}
}
]);

常见面试问题

Q1: React Router 的实现原理是什么?

答案

React Router 基于监听 URL 变化条件渲染实现:

// 简化版实现原理
import { createContext, useContext, useState, useEffect } from 'react';

// 1. 创建 Router Context
const RouterContext = createContext<{
pathname: string;
navigate: (to: string) => void;
} | null>(null);

// 2. BrowserRouter 监听 popstate
function BrowserRouter({ children }: { children: React.ReactNode }) {
const [pathname, setPathname] = useState(window.location.pathname);

useEffect(() => {
const handlePopState = () => {
setPathname(window.location.pathname);
};

window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, []);

const navigate = (to: string) => {
window.history.pushState({}, '', to);
setPathname(to);
};

return (
<RouterContext.Provider value={{ pathname, navigate }}>
{children}
</RouterContext.Provider>
);
}

// 3. Route 条件渲染
function Route({ path, element }: { path: string; element: React.ReactNode }) {
const { pathname } = useContext(RouterContext)!;

// 简单匹配(实际实现更复杂)
if (pathname === path) {
return <>{element}</>;
}
return null;
}

// 4. Link 改变 URL
function Link({ to, children }: { to: string; children: React.ReactNode }) {
const { navigate } = useContext(RouterContext)!;

const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
navigate(to);
};

return <a href={to} onClick={handleClick}>{children}</a>;
}

Q2: BrowserRouter 和 HashRouter 的区别?

答案

特性BrowserRouterHashRouter
URL/users/123/#/users/123
APIhistory.pushStatelocation.hash
服务器需配置 fallback无需配置
SEO友好不友好
原理监听 popstate监听 hashchange
// BrowserRouter 需要服务器配置
// Nginx 示例
// location / {
// try_files $uri $uri/ /index.html;
// }

// HashRouter 无需配置,因为 # 后的内容不会发送到服务器

Q3: 如何实现路由懒加载?

答案

import { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router-dom';

// 方式1: React.lazy + Suspense
const Dashboard = lazy(() => import('./Dashboard'));

const router = createBrowserRouter([
{
path: '/dashboard',
element: (
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
)
}
]);

// 方式2: route.lazy(推荐,v6.4+)
const router = createBrowserRouter([
{
path: '/dashboard',
lazy: () => import('./Dashboard').then(m => ({ Component: m.default }))
}
]);

// 方式3: 预加载
const DashboardPromise = import('./Dashboard');
const Dashboard = lazy(() => DashboardPromise);

// 鼠标悬停时预加载
<Link to="/dashboard" onMouseEnter={() => import('./Dashboard')}>
控制台
</Link>

Q4: 如何实现路由守卫/权限控制?

答案

// 1. 认证守卫组件
function RequireAuth({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
const location = useLocation();

if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}

return <>{children}</>;
}

// 2. 在路由配置中使用
const router = createBrowserRouter([
{
element: <RequireAuth><Outlet /></RequireAuth>,
children: [
{ path: '/dashboard', element: <Dashboard /> },
{ path: '/profile', element: <Profile /> }
]
}
]);

// 3. 结合 loader 进行权限校验
async function adminLoader() {
const user = await getUser();
if (!user || !user.isAdmin) {
throw redirect('/unauthorized');
}
return user;
}

const router = createBrowserRouter([
{
path: '/admin',
element: <AdminPanel />,
loader: adminLoader
}
]);

Q5: loader 和 useEffect 获取数据有什么区别?

答案

特性loaderuseEffect
时机路由切换前组件挂载后
阻塞阻塞渲染不阻塞
骨架屏需要配合 defer天然支持
错误处理errorElementtry/catch
数据位置路由级别组件级别
并行请求自动并行需手动 Promise.all
// loader: 数据准备好再渲染
{
path: '/users/:id',
element: <UserDetail />,
loader: async ({ params }) => {
return fetch(`/api/users/${params.id}`).then(r => r.json());
}
}

// useEffect: 先渲染再请求
function UserDetail() {
const [user, setUser] = useState(null);
const { id } = useParams();

useEffect(() => {
fetch(`/api/users/${id}`)
.then(r => r.json())
.then(setUser);
}, [id]);

if (!user) return <Loading />;
return <div>{user.name}</div>;
}

相关链接