
一句话:v6 的核心变化是「
Routes替代Switch」「element={<Comp />}替代component={Comp}」「相对路径默认相对于父路由」「用Outlet渲染子路由」。
一、安装与基本结构
npm i react-router-dom
// main.tsx
import { BrowserRouter } from 'react-router-dom';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
// App.tsx
import { Routes, Route } from 'react-router-dom';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
}
| Router 组件 | 说明 |
|---|---|
BrowserRouter |
用 HTML5 History API(路径形如 /about),需要服务端 fallback 到 index.html |
HashRouter |
用 URL hash(路径形如 #/about),不需要服务端配置,但 URL 不美观、SEO 差 |
MemoryRouter |
内存路由(不改变 URL),用于测试与 React Native |
StaticRouter |
用于 SSR(需要传入 location) |
createBrowserRouter |
v6.4+ 的数据路由(推荐用于新项目) |
二、Routes 与 Route
<Routes>
{/* ① 基本路由 */}
<Route path="/" element={<Home />} />
{/* ② 动态参数 */}
<Route path="/users/:id" element={<UserDetail />} />
{/* ③ 嵌套路由(子路由在父路由的 element 里用 <Outlet /> 渲染) */}
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} /> {/* ⭐ index 路由 */}
<Route path="settings" element={<Settings />} /> {/* ⭐ 相对路径(等价 /dashboard/settings) */}
<Route path="profile" element={<Profile />} />
</Route>
{/* ④ 通配符(404 兜底) */}
<Route path="*" element={<NotFound />} />
{/* ⑤ 多段通配 */}
<Route path="/docs/*" element={<DocsLayout />} />
</Routes>
⚠️ v5 与 v6 的关键差异:
v5:
<Switch>
<Route exact path="/" component={Home} />
<Route path="/users/:id" render={(props) => <User {...props} />} />
</Switch>
v6:
<Routes>
<Route path="/" element={<Home />} /> {/* ⭐ 默认就是精确匹配 */}
<Route path="/users/:id" element={<UserDetail />} />
</Routes>
变化点:
① Switch → Routes
② component / render → element(传 JSX 元素)
③ 不再需要 exact(v6 的匹配更严格,且靠「路由树」结构决定)
④ 相对路径:v6 中子路由的 path 是【相对于父路由】的
⑤ props 注入(history / location / match)→ 改用 Hooks(useNavigate / useLocation / useParams)
import { Link, NavLink } from 'react-router-dom';
// ① Link:不会刷新页面的「a 标签」
<Link to="/about">关于</Link>
<Link to={`/users/${id}`}>详情</Link>
<Link to="/about" replace>关于</Link> {/* replace:不留下历史记录 */}
<Link to="/search" state={{ from: 'home' }}>搜索</Link> {/* 传状态 */}
<Link to={`/items/${id}`} relative="path">详情</Link>
// ② NavLink:会自动「标记激活状态」
<NavLink
to="/about"
className={({ isActive, isPending, isTransitioning }) =>
isActive ? 'nav-link active' : 'nav-link'
}
style={({ isActive }) => ({ fontWeight: isActive ? 'bold' : 'normal' })}
>
关于
</NavLink>
// ③ NavLink 的 isActive 判定规则
// 默认:当前路径「是该 to 的路径 或 以其为前缀」时激活
<NavLink to="/users">用户</NavLink>
// 匹配 /users 和 /users/123
// ✅ 需要「精确匹配」用 end
<NavLink to="/" end>首页</NavLink>
// 匹配 / 但不匹配 /about
| 组件 | 用途 | 特点 |
|---|---|---|
Link |
导航 | 不刷新页面、保留 SPA 体验 |
NavLink |
导航栏链接 | 自动提供 isActive |
<a href> |
整页跳转 | 会刷新(不推荐在 SPA 内使用) |
import { Navigate } from 'react-router-dom';
// ① 声明式重定向(渲染时跳转)
function OldPage() {
return <Navigate to="/new-page" replace />;
}
// ② 条件重定向(未登录 → 登录页)
function RequireAuth({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
// ③ 在路由里配置重定向
<Routes>
<Route path="/old" element={<Navigate to="/new" replace />} />
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
五、Outlet(渲染子路由)
import { Outlet } from 'react-router-dom';
// 父路由组件:提供「布局」,用 Outlet 渲染匹配到的子路由
function DashboardLayout() {
return (
<div className="dashboard">
<Sidebar />
<main>
<Outlet /> {/* ⭐ 子路由在这里渲染 */}
</main>
</div>
);
}
// 配套的路由配置
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} /> {/* /dashboard */}
<Route path="settings" element={<Settings />} /> {/* /dashboard/settings */}
<Route path="users">
<Route index element={<UserList />} /> {/* /dashboard/users */}
<Route path=":id" element={<UserDetail />} /> {/* /dashboard/users/123 */}
</Route>
</Route>
</Routes>
// Outlet 还可以传 context(给子路由用 useOutletContext 读取)
function DashboardLayout() {
const [user] = useState(loadUser);
return (
<div>
<Sidebar />
<Outlet context={{ user }} /> {/* ⭐ 传上下文 */}
</div>
);
}
function Overview() {
const { user } = useOutletContext<{ user: User }>();
return <div>{user.name}</div>;
}
六、常用 Hooks
import {
useParams,
useNavigate,
useLocation,
useSearchParams,
useOutletContext,
useMatch,
useResolvedPath,
} from 'react-router-dom';
// ① useParams:读取动态参数
function UserDetail() {
const { id } = useParams<{ id: string }>();
return <div>用户 {id}</div>;
}
// ② useNavigate:编程式导航
function LoginButton() {
const navigate = useNavigate();
const handleLogin = async () => {
await login();
navigate('/dashboard'); // 跳转
// navigate(-1); // 后退
// navigate('/dashboard', { replace: true }); // 替换历史
// navigate('/dashboard', { state: { from: 'login' } });
// navigate('settings'); // ⭐ 相对路径(当前路由下的 settings)
// navigate('../'); // 上一级
};
return <button onClick={handleLogin}>登录</button>;
}
// ③ useLocation:读取当前位置
function Analytics() {
const location = useLocation();
useEffect(() => {
track(location.pathname + location.search); // ⭐ 路由变化时上报
}, [location]);
return null;
}
// ④ useSearchParams:读写查询参数
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get('q') ?? '';
const page = Number(searchParams.get('page') ?? '1');
const updateKeyword = (q: string) => {
setSearchParams({ q, page: '1' }); // ⭐ 会更新 URL 并触发重渲染
// setSearchParams((prev) => { prev.set('q', q); return prev; });
};
return <input value={keyword} onChange={(e) => updateKeyword(e.target.value)} />;
}
// ⑤ useMatch:手动匹配某条路径(进阶)
function Component() {
const match = useMatch('/users/:id');
console.log(match?.params.id);
return null;
}
| Hook | 作用 | 对应 v5 的 props |
|---|---|---|
useParams |
动态参数 | props.match.params |
useNavigate |
编程式导航 | props.history.push |
useLocation |
当前位置信息 | props.location |
useSearchParams |
查询参数 | 自己解析 location.search |
useOutletContext |
读取父路由通过 Outlet 传的 context |
— |
useMatch |
手动匹配路径 | props.match |
七、v5 → v6 迁移对照
| v5 | v6 |
|---|---|
<Switch> |
<Routes> |
<Route exact path component={X} /> |
<Route path element={<X />} /> |
<Route render={...} /> / children |
element={<Comp />} |
<Redirect to> |
<Navigate to replace /> |
{ match, location, history } props |
useParams / useLocation / useNavigate |
history.push() |
navigate() |
useHistory() |
useNavigate() |
withRouter() |
直接用 Hooks |
| 子路由必须写完整路径 | 相对路径(相对于父路由) |
<Route path="/" exact> |
不需要 exact(匹配更严格) |
useRouteMatch() |
useMatch() |
八、常见坑
// ① 用 <a href> 做站内跳转(导致整页刷新)
<a href="/about">关于</a> // ❌
<Link to="/about">关于</Link> // ✅
// ② 嵌套路由忘记在父组件里写 <Outlet />
<Route path="/dashboard" element={<Layout />}>
<Route path="settings" element={<Settings />} />
</Route>;
function Layout() {
return <div><Sidebar /></div>; // ❌ 子路由没有渲染位置
// ✅ return <div><Sidebar /><Outlet /></div>;
}
// ③ NavLink 的激活判断不符合预期(忘了 end)
<NavLink to="/">首页</NavLink> // ⚠️ 所有路径都会激活(因为 / 是前缀)
<NavLink to="/" end>首页</NavLink> // ✅
// ④ BrowserRouter 部署后刷新 404(服务端没配 fallback)
// ✅ Nginx: try_files $uri $uri/ /index.html;
// ✅ 或用 HashRouter
// ⑤ 路由参数变化时组件不重新请求数据
function UserDetail() {
const { id } = useParams();
useEffect(() => {
fetchUser(id);
}, []); // ❌ 缺 id 依赖 → 切换用户不重新请求
// ✅ 依赖写 [id]
}
// ⑥ 在同一路径上「参数变化」但组件不重新挂载
<Route path="/users/:id" element={<UserDetail />} />
// 从 /users/1 到 /users/2:React 复用同一个组件实例(因为 element 类型相同)
// → 内部 state 不会重置(如果需要重置,给 element 加 key)
<Route path="/users/:id" element={<UserDetail key={...} />} /> // 需要动态 key
// ⑦ 通配符位置错误(* 应该放在最后)
<Route path="*" element={<NotFound />} />
// ⚠️ 放在前面会吞掉所有路由
// ⑧ 在「非路由组件」里用路由 Hooks(没有 Router 包裹 → 报错)
// ⑨ 多层嵌套时 path 写成了绝对路径(v6 里子路由应为相对路径)
<Route path="/dashboard" element={<Layout />}>
<Route path="/dashboard/settings" element={<S />} /> // ⚠️ v6 里这样写会拼成 /dashboard/dashboard/settings
<Route path="settings" element={<S />} /> // ✅
</Route>
// ⑩ 用 useSearchParams 但忘了「它返回的是只读快照 + setter」
const [params] = useSearchParams();
params.set('q', 'x'); // ❌ 不会更新 URL
setSearchParams({ q: 'x' }); // ✅
// ⑤ 与 ⑥ 的完整修复示例
function UserDetail() {
const { id } = useParams<{ id: string }>();
// ✅ 依赖 id → 切换用户时重新请求
const { data, isLoading } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id!),
});
return isLoading ? <Skeleton /> : <div>{data?.name}</div>;
}
// 如果组件内部有「不该跨用户保留」的状态 → 用 key 强制重建
function UserDetailRoute() {
const { id } = useParams<{ id: string }>();
return <UserDetailInner key={id} />;
}
九、面试延伸
- 「React Router v6 有哪些核心 API?」
组件:Routes(替代 v5 的Switch)、Route(用element传 JSX)、Link/NavLink(导航,NavLink提供isActive)、Navigate(重定向)、Outlet(渲染子路由)。Hooks:useParams(动态参数)、useNavigate(编程式导航)、useLocation(当前位置)、useSearchParams(查询参数)、useOutletContext(读取父路由的 context)。v6.4+ 还有数据路由(createBrowserRouter)与loader/action。 -
「v6 相比 v5 有哪些重要变化?」
五点:①Switch→Routes;②component/render→element(传 JSX 元素);③ 不再需要exact(匹配更严格,靠路由树结构决定);④ 子路由的路径是「相对」的(path="settings"会拼成/parent/settings);⑤ 不再注入路由 props(history/location/match),改用 Hooks。此外Redirect变成Navigate,withRouter被移除。 -
「
Link和<a>有什么区别?」
<a href>会触发浏览器整页跳转(重新加载 HTML、重新执行 JS),在 SPA 里这等于「放弃单页应用的优势」(白屏、状态丢失、重复下载资源)。<Link>是 React Router 的组件——它内部调用history.pushState并阻止默认行为,因此在不刷新页面的情况下更新 URL 与渲染视图。站内导航都应该用Link/NavLink/useNavigate。 -
「嵌套路由是怎么工作的?」
v6 用「路由树 +Outlet」:父路由的element是「布局组件」,其中用<Outlet />标记「子路由的渲染位置」。子路由的path是相对的(相对于父路由)。匹配/dashboard/settings时,React Router 会渲染「DashboardLayout(外层)+Settings(在Outlet处)」。好处是布局可以跨页面复用,且「导航栏保持稳定、只有内容区切换」。此外index路由表示「父路由路径本身」的默认子路由。 -
「路由参数变化时组件会重新挂载吗?」
不会。同一路由下参数变化(/users/1→/users/2)时,React Router 渲染的是同一个<Route path="/users/:id" element={<UserDetail />} />,React 看到「元素类型相同」就复用组件实例——所以内部的state、滚动位置等不会重置。要处理这种情况有两种方式:①useEffect依赖参数(重新请求数据);② 需要「重置内部状态」时,给 element 加key(key={id})强制重建。 -
「
useSearchParams和URLSearchParams是什么关系?」
useSearchParams返回[searchParams, setSearchParams],其中searchParams是一个URLSearchParams实例(只读快照,提供get/getAll/has/toString等方法);setSearchParams用于更新 URL(接受对象、字符串或「修改函数」)。关键是:直接修改searchParams不会生效(它是快照),必须调用setSearchParams才能更新 URL 并触发重渲染。 -
「为什么用
BrowserRouter部署后刷新会 404?」
因为BrowserRouter使用 HTML5 History API,URL 形如/dashboard/settings——这是「虚拟路径」,服务器上并没有这个文件。刷新时浏览器直接请求/dashboard/settings,服务器找不到对应资源就返回 404。解法:① 配置服务端 fallback(把所有未匹配的请求返回index.html,如 Nginx 的try_files $uri $uri/ /index.html;);② 或改用HashRouter(路径在#后面,服务器只看到/,不需要额外配置,但 URL 不美观、SEO 更差)。 -
「
NavLink的默认激活规则是什么?什么时候需要end?」
默认规则是「当前路径等于to,或者以to为前缀」就激活。所以<NavLink to="/">首页</NavLink>会在所有路径下都激活(因为所有路径都以/开头),这通常不是期望的。需要「精确匹配」时加end:<NavLink to="/" end>首页</NavLink>。同理<NavLink to="/users" />会在/users/123下也激活(这通常是期望的,因为表示「用户在某个用户的详情页,侧栏的「用户」应该高亮」)。
一句话速记
v6 核心:
Routes替代Switch、element={<Comp />}替代component、不需要exact、子路由用「相对路径」+<Outlet />渲染、路由信息改用 Hooks(useParams/useNavigate/useLocation/useSearchParams);站内导航用Link/NavLink(别用<a>,会整页刷新)、重定向用Navigate、导航栏高亮用NavLink(to="/"要加end);参数变化不会重新挂载组件(useEffect依赖参数,或加key强制重建);BrowserRouter需要服务端 fallback(否则刷新 404),或改用HashRouter。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!