主题
其他hook与react-router-dom 路由(Day8 1/3)
本页对应 React + TypeScript + 医通项目,共 6 段视频,约 45 分钟。
学习目标
- 能够用自己的话解释前端路由的本质是 URL 与路由组件之间的 key-value 映射,并说明其在 SPA 单页面应用中实现无刷新页面切换的作用。
- 能够独立完成
react-router-dom的安装与基础配置:在入口文件中使用BrowserRouter包裹App,并在App中使用Routes、Route的path与element配置多个路由。 - 能够使用
Navigate将根路径重定向到指定首页路径,并能说明直接为多个路径绑定同一组件与使用重定向的差异。 - 能够创建 404 页面组件,并通过
path="*"通配符路由配置不存在路径的兜底展示。 - 能够使用
Link和NavLink实现无刷新跳转,区分普通a标签刷新页面的问题,并利用NavLink的active状态自定义激活高亮样式。
知识点
本节笔记标题为“其他hook”,但转写稿实际围绕前端路由与 React Router 的基本使用展开;下面只按转写稿整理,不额外补充未讲到的 hook。转写稿中的“ros/root/bsser router”等按代码语义统一为 Routes、Route、BrowserRouter。
路由本质与 SPA:URL 到组件的 key-value 映射
为什么需要:单页面应用只有一个 HTML,却要在一个“舞台”里切换不同页面内容,不能每次跳转都刷新整页。怎么想:路由本质是 key-value 映射,key 是 URL 地址,value 是对应的 React 路由组件;访问某个 URL,就在页面里展示对应组件。转写稿用路由器端口类比:端口是 key,端口后面的电脑是 value;URL 是 key,路由组件是 value。怎么做:把这种映射配置到路由容器中,地址变化时由前端路由切换组件,而不是请求新 HTML。易错点:不要把“路由组件”理解成新开一个 HTML 页面;SPA 仍是同一个 HTML,通过 JS 切换内容。访问根路径时如果没配映射,页面就会空白,这不是路由坏了,而是没有对应 value。
BrowserRouter、Routes、Route 与安装包裹
为什么需要:有了映射关系,还要有“路由器”承载它,并让后代组件具备路由切换能力。怎么想:BrowserRouter 是容器组件,类似 Context 的 Provider,只有被它包裹的后代才能进行路由切换;Routes 是路由配置和展示出口,写在哪里,匹配到的组件就替换到哪里;Route 用 path 指定 URL,用 element 指定要展示的路由组件。怎么做:在项目根目录安装,再在入口文件用 BrowserRouter 包住 App。易错点:安装命令要在项目根目录执行;BrowserRouter 一般包 App,不是只包某一个页面;Routes 的位置决定组件展示位置。
bash
npm i react-router-domjsx
// index.js
import { BrowserRouter } from "react-router-dom";
<BrowserRouter>
<App />
</BrowserRouter>配置 Route、创建路由组件与目录约定
为什么需要:要访问 /login、/home、/user 时分别展示不同组件,必须创建组件并配置映射。怎么想:有 path/element 配置、能参与路由映射的组件是路由组件;没有路径映射的是普通组件。路由组件一般放 src/pages 或 src/views,普通组件放 src/components。怎么做:创建 Login、Home、User,在 App 中用 Routes 和 Route 配置。调试时访问 localhost:3000/login 显示 Login,/home 显示 Home,/user 显示 User;访问根路径 / 没有配置,所以空白。易错点:不要给同一个组件同时硬配 / 和 /home 两个路径,容易乱;根路径空白应使用重定向解决。
jsx
// App.jsx
import { Routes, Route, Navigate, NavLink, Link } from "react-router-dom";
import Login from "./pages/Login";
import Home from "./pages/Home";
import User from "./pages/User";
import NotFound from "./pages/NotFound";
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/home" element={<Home />} />
<Route path="/user" element={<User />} />
{/* 根路径重定向:三种写法三选一 */}
<Route path="/" element={<Navigate to="/home" />} />
{/* <Route index={true} element={<Navigate to="/home" />} /> */}
{/* <Route index element={<Navigate to="/home" />} /> */}
<Route path="*" element={<NotFound />} />
</Routes>Navigate 重定向:根路径自动去首页
为什么需要:用户访问网站根目录 / 时,通常希望看到首页,而不是空白。怎么想:不要给 Home 同时配两个路径,而是访问 / 时用 Navigate 重定向到 /home,保持一个 URL 对应一个路由组件的清晰关系。怎么做:在根路径的 element 中放 <Navigate to="/home" />。转写稿还给出等价写法:index={true},以及值为真时可省略成 index。调试时刷新根路径,会自动跳到 /home 并展示 Home。易错点:Navigate 的跳转地址属性是 to,不是 href;三种写法不要同时启用,选一种即可。
404 页面与通配符
为什么需要:用户可能输错地址,访问不存在的路径;没有匹配时页面空白会让人困惑。怎么想:创建一个 NotFound 路由组件,再用通配符路径接住所有未匹配地址。怎么做:配置 <Route path="*" element={<NotFound />} />。调试时访问不存在路径显示 404,访问已配置路径仍显示对应组件。易错点:* 是通配符,表示匹配范围很广;路由配置中放在末尾更直观。转写稿还联系正则:* 零个或多个,+ 一个或多个,? 零个或一个,别把含义记混。
NavLink、Link 无刷新跳转与 active 高亮
为什么需要:普通 <a> 标签默认会跳转并刷新页面,不符合 SPA 无刷新切换。怎么想:用 React Router 提供的 Link、NavLink,它们最终也渲染成 <a>,但内部阻止默认跳转行为;NavLink 在被激活时额外带 active 类名,可用于高亮,Link 生成的则是干净 <a>。怎么做:用 to 指定地址,替换普通 href;给 .active 写高亮样式。调试时点普通 <a> 会刷新,点 NavLink/Link 无刷新;当前路由对应的 NavLink 带 active。易错点:NavLink 的 N、L 大写;跳转属性是 to;需要高亮选 NavLink,不需要高亮可用 Link。
jsx
<NavLink to="/home">首页</NavLink>
<NavLink to="/login">登录页</NavLink>
<NavLink to="/user">用户中心</NavLink>
<Link to="/home">首页(Link)</Link>css
/* App.css */
.active {
color: red;
}路由的本质与 SPA
为什么需要:SPA(Single Page Application,单页面应用)只有一个 HTML 页面,却要展示不同内容,不能靠整页刷新。
怎么想:路由本质是 key-value 映射:key 是 URL,value 是 React 路由组件。就像路由器端口连电脑,一个端口对应一台电脑;访问某个 URL,就展示对应组件。
怎么做:访问 localhost:3000/login 展示 Login,访问 /home 展示 Home。
易错点:SPA 不是“配置页面”,而是单页面应用;路由切换是替换组件,不是重新加载 HTML。
基本概念与使用步骤
为什么需要:组件必须被路由容器包裹,才能参与路由切换。
怎么想:BrowserRouter 是容器组件;Routes 相当于路由器;Route 是一条映射,path 指定 URL,element 指定路由组件。转写稿强调:Routes 写在哪里,匹配到的组件就替换到哪里。
怎么做:
jsx
// 1. npm i react-router-dom
// 2. index.js
import { BrowserRouter } from 'react-router-dom'
<BrowserRouter>
<App />
</BrowserRouter>
// 3. App.jsx
<Routes>
<Route path='/login' element={<Login />} />
<Route path='/home' element={<Home />} />
</Routes>路由组件一般放 src/pages 或 src/views,普通组件放 src/components。
易错点:只有被 BrowserRouter 包裹的组件及其后代才能切换;路由组件有 URL 映射配置,普通组件没有。
重定向与 404
为什么需要:访问 / 时若没配置会空白;用户输错地址也需要提示。
怎么想:/ 重定向到 /home;* 匹配不到时展示 404。
怎么做:
jsx
<Route index element={<Navigate to='/home' />} />
<Route path='*' element={<PageNotFound />} />index 等价于 path='/',布尔值为真时可省略 ={true}。
易错点:不要让 / 和 /home 都直接渲染 Home,否则一个组件对应两个 URL,关系会乱;404 用 * 通配兜底。
NavLink、Link 与高亮
为什么需要:普通 <a> 会刷新页面,不符合 SPA 无刷新切换。
怎么想:Link 生成 a 链接并阻止默认刷新;NavLink 在激活时默认带 active 类名。
怎么做:
jsx
import { NavLink, Link } from 'react-router-dom'
<NavLink to="/home">首页</NavLink>
<Link to="/home">首页</Link>
<NavLink
to="/home"
className={({ isActive }) => isActive ? 'myselfActive' : ''}
>首页</NavLink>调试时可看到:NavLink 激活时 a 标签多出 active,Link 没有。
易错点:NavLink/Link 用 to,不是 href;自定义类名回调要返回字符串。
路由表、二级路由与 Outlet
为什么需要:路由多时 JSX 配置冗长;页面常有父子嵌套。
怎么想:路由表是描述映射关系的数组,useRoutes(routes) 等价于生成 <Routes>;子路由用 children,父路由用 <Outlet /> 占位。
怎么做:
js
const routes = [
{ path: '/login', element: <Login /> },
{ path: '/home', element: <Home /> },
{ index: true, element: <Navigate to='/home' /> },
{ path: '*', element: <PageNotFound /> }
]
// 二级路由
{
path: '/home',
children: [
{ path: 'news', element: <News /> } // 不要写 /news
]
}易错点:子路由 path 前不要加 /,否则会被当成一级路由;父组件要放 <Outlet /> 才能显示子路由。
编程式导航与传参
为什么需要:跳转前可能要判断 token、权限等条件,链接点击无法写逻辑。
怎么想:useNavigate() 得到跳转函数;传参分 path、query、state。
怎么做:
jsx
let navigate = useNavigate()
if (localStorage.getItem('token')) navigate('/user')
navigate('/user', { replace: true })
navigate(-1) // 后退jsx
// path 参数:路由需占位符
<Route path='/home/message/path/:id/:school' element={<PathTest />} />
let { id, school } = useParams()
// query 参数
<Link to="/home/message/query?username=atguigu&age=20">query参数</Link>
let [searchQuery] = useSearchParams()
let username = searchQuery.get('username')
// state 参数
<Link to='/home/message/state' state={数据}>state参数</Link>
let { state: { id, title, isDone } } = useLocation()易错点:path 参数必须配置 :id 占位符;query 无需占位符;navigate(url) 默认新增历史记录,replace:true 才替换当前记录。
懒加载与两种路由模式
为什么需要:路由组件多时首屏包大,按需加载更快。
怎么想:React.lazy 动态导入组件,Suspense 在未加载完成时显示 fallback。
怎么做:
jsx
import { lazy, Suspense } from 'react'
const About = lazy(() => import('../pages/About'))
function load(Com) {
return (
<Suspense fallback={<div>组件正在加载中....</div>}>
<Com />
</Suspense>
)
}
// element: load(About)BrowserRouter 是 HTML5 history 模式:localhost:3000/home/news;HashRouter 是 hash 模式:localhost:3000/#/home/news。
易错点:懒加载组件必须用 Suspense 包裹;Hash 模式地址栏会带 #,不要和 history 模式混淆。
材料边界与同音字纠正
本节笔记只有两条核心信息:redux-toolkit 是官方提供的 Redux 工具包,作用是对数据状态进行集中式管理;react-redux 是在 React 中更方便使用 Redux 的绑定库。需要先说明:你给的转写稿实际讲的是“前端路由”,出现了 BrowserRouter、Route、Navigate、NavLink、404 配置等内容,并没有讲 redux-toolkit。因此不能把转写稿里的“路由本质是 key-value 映射”“单页面应用切换”直接当成 Redux 的推导过程,否则就是编造。转写稿中“roos/root/ros”等是同音转写问题,实际应理解为 router、route 等路由概念,不是 Redux 的 store。本节笔记也没有附图片和代码,所以下面不虚构图片与代码,只把笔记中的两个知识点扩写成概念教程。
redux-toolkit 为什么需要
当页面和组件越来越多时,如果状态只放在某个组件内部,就会出现“兄弟组件要通信、深层组件要拿数据、多个页面要共享同一份数据”的问题。层层传 props 会让组件耦合变重,状态改动也分散在多个地方,排查问题时很难知道“是谁改了数据”。笔记说它的作用是“对数据状态进行集中式管理”,意思就是把需要共享、需要统一修改规则的数据放到一个集中位置管理,而不是每个组件各自维护一份。这样做的价值是:数据来源更清晰,修改入口更集中,组件只负责展示和触发修改。
redux-toolkit 怎么想
redux-toolkit 不是另一个全新的状态思想,而是官方提供的 Redux 工具包。可以把它理解成“官方给 Redux 使用方式做的整理和封装”,目的是让集中式状态管理更容易落地。这里要抓住两个关键词:第一是“官方”,说明它和 Redux 生态一致,不是野路子;第二是“工具包”,说明它服务于 Redux 的集中管理,而不是替代 React 本身。怎么想这一层:先判断状态是否真的需要集中管理。只有跨组件、跨页面共享,且修改规则需要统一的状态,才适合放进集中式 store;纯展示、只在一个组件内部使用的临时状态,不必强行放进去。
redux-toolkit 怎么做
按笔记的范围,做法可以概括为概念步骤:先明确要集中管理的数据状态有哪些;再为这些状态设计清晰的修改入口,避免组件随意直接改数据;然后把集中状态组织到 Redux 的 store 中;最后让 React 组件通过绑定库读取状态、触发修改。具体 API 名称和写法要以项目所用版本和官方文档为准,本节笔记没有给出代码,因此不补写 configureStore、createSlice 等代码细节。调试时也不应像转写稿里的路由那样看 URL 是否切换,而应看集中状态是否按预期变化、组件是否订阅到了变化。若转写稿里的“路径对应组件”拿来类比,只能类比“入口对应结果”,不能当作 Redux 的真实推导。
react-redux 为什么需要
Redux 负责集中管理状态,但 React 组件并不会自动知道 store 在哪里,也不会自动在状态变化时重新渲染。笔记说 react-redux 是“在 React 中更方便的使用 Redux”,原因就在这里:它负责把 Redux 的集中状态和 React 组件连接起来。没有这层绑定,组件访问 store、订阅变化、派发修改都会更麻烦;有了它,React 中读取和修改集中状态的方式更符合组件开发习惯。
react-redux 怎么做与易错点
做法上,核心思路是:把集中管理的 store 提供给 React 应用,让需要数据的组件从连接层读取状态,让需要修改状态的组件通过连接层触发修改。具体使用哪种组件或 hook,应以当前 react-redux 版本文档为准,本节笔记不展开代码。易错点有三个:第一,把所有状态都塞进 Redux,导致简单问题复杂化;第二,组件直接改集中状态,绕过统一修改入口;第三,把 react-redux 当成 Redux 本身,实际上前者是 React 绑定层,后者才是集中状态管理工具。纠正同音字时也要注意:不要看到转写稿里的“root”就误认为 Redux,二者不是同一个概念。
课堂画面






代码示例
示例一:基础路由配置(BrowserRouter + Routes/Route + 重定向 + 404 + NavLink 高亮)
jsx
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
)jsx
// src/pages/Home.jsx
import React from 'react'
export default function Home() {
return <div>Home组件的内容</div>
}
// src/pages/Login.jsx
import React from 'react'
export default function Login() {
return <div>Login组件的内容</div>
}
// src/pages/User.jsx
import React from 'react'
export default function User() {
return <div>User组件的内容</div>
}
// src/pages/PageNotFound.jsx
import React from 'react'
export default function PageNotFound() {
return <div>404 页面找不到</div>
}jsx
// src/App.jsx
import React from 'react'
import { Routes, Route, Navigate, NavLink, Link } from 'react-router-dom'
import Home from './pages/Home'
import Login from './pages/Login'
import User from './pages/User'
import PageNotFound from './pages/PageNotFound'
import './App.css'
export default function App() {
return (
<div>
<h4>NavLink 跳转标签组件 高亮样式 active</h4>
<ul>
<li><NavLink to="/home">首页</NavLink></li>
<li><NavLink to="/login">登录页</NavLink></li>
<li><NavLink to="/user">用户中心</NavLink></li>
</ul>
<h4>NavLink 自定义高亮样式类名</h4>
<ul>
<li><NavLink to="/home" className={({isActive}) => {
return isActive ? 'myselfActive' : ''
}}>首页</NavLink></li>
<li><NavLink to="/login" className={({isActive}) => {
return isActive ? 'myselfActive' : ''
}}>登录页</NavLink></li>
<li><NavLink to="/user" className={({isActive}) => {
return isActive ? 'myselfActive' : ''
}}>用户中心</NavLink></li>
</ul>
<h4>Link 跳转组件,没有 高亮样式</h4>
<ul>
<li><Link to="/home">首页</Link></li>
<li><Link to="/login">登录页</Link></li>
<li><Link to="/user">用户中心</Link></li>
</ul>
{/* 当对应的路由激活的时候,将对应路由组件替换掉 <Routes> 组件所在位置 */}
<Routes>
<Route path='/login' element={<Login/>}></Route>
<Route path='/home' element={<Home/>}></Route>
<Route path='/user' element={<User/>}></Route>
{/* 重定向:访问根目录时去首页 */}
<Route index element={<Navigate to='/home' />}></Route>
{/* 404:没有 url 匹配成功的时候,会匹配到 * */}
<Route path='*' element={<PageNotFound/>}></Route>
</Routes>
</div>
)
}css
/* src/App.css */
a { text-decoration: none; color: #333; }
.active { color: red; } /* NavLink 默认高亮类名 */
.myselfActive { color: blue; } /* 自定义高亮类名 */关键行与易错点
BrowserRouter是容器组件,只有被它包裹的组件及其后代组件才能进行路由切换,所以必须包在App外层(一般写在index.js)。<Routes>相当于路由器,<Route>才是路由;path指定 url,element指定路由组件。路由组件会被替换到<Routes>所在的位置,写哪儿显示哪儿。index element={<Navigate to='/home' />}等价于path='/'的重定向;不要直接给/再配一个Home,否则一个 url 对应两个组件会很乱。index是index={true}的简写。path='*'是通配符,放在最后兜底 404;匹配成功时仍然优先展示正常路由。NavLink和Link最终都渲染成a标签,区别是NavLink在路由激活时会自动加上active类名,Link没有。用普通<a>会刷新页面,必须用这两个组件才能无刷新跳转。className传回调函数时,参数对象里有isActive布尔值,返回的字符串才是最终的类名,别写成className='myselfActive'。
示例二:路由表 useRoutes + 二级路由 Outlet + 编程式导航
jsx
// src/routes/index.js
import React from 'react'
import { Navigate } from 'react-router-dom'
import Login from '../pages/Login'
import Home from '../pages/Home'
import User from '../pages/User'
import PageNotFound from '../pages/PageNotFound'
import News from '../pages/News'
import Message from '../pages/Message'
const routes = [
{ path: '/login', element: <Login /> },
{
path: '/home',
element: <Home />,
// 二级路由
children: [
// 简写:path:'news',前面不能有 / ,否则会认为是一级路由
{ path: 'news', element: <News /> },
{ path: 'message', element: <Message /> }
]
},
{ path: '/user', element: <User /> },
// 重定向
{ index: true, element: <Navigate to='/home' /> },
// 404
{ path: '*', element: <PageNotFound /> }
]
export default routesjsx
// src/App.jsx
import React from 'react'
import { useRoutes } from 'react-router-dom'
import routes from './routes'
export default function App() {
// useRoutes(路由表数组) ===> <Routes><Route></Route></Routes>
const element = useRoutes(routes)
return <div>{element}</div>
}jsx
// src/pages/Home.jsx
import React from 'react'
import { NavLink, Outlet } from 'react-router-dom'
export default function Home() {
return (
<div>
<h3>Home组件</h3>
<ul>
<li><NavLink to="/home/news">新闻</NavLink></li>
<li><NavLink to="/home/message">消息</NavLink></li>
</ul>
{/* 展示二级路由 */}
<Outlet />
</div>
)
}
// src/pages/News.jsx
import React from 'react'
export default function News() {
return <div>News组件的内容</div>
}jsx
// src/pages/User.jsx
import React from 'react'
import { useNavigate } from 'react-router-dom'
export default function User() {
const navigate = useNavigate()
const toLogin = () => {
if (localStorage.getItem('token')) {
navigate('/user')
} else {
// 替换当前历史记录跳转
navigate('/login', { replace: true })
}
}
return (
<div>
<h3>User组件</h3>
<button onClick={toLogin}>进入用户中心</button>
<button onClick={() => navigate(-1)}>后退</button>
<button onClick={() => navigate(1)}>前进</button>
</div>
)
}关键行与易错点
- 路由表本质是“url → 路由组件”的映射数组,
useRoutes(routes)等价于<Routes><Route/></Routes>,但useRoutes必须在BrowserRouter内部调用(所以App要被包裹)。 - 二级路由必须写在父路由的
children里,子路由path前面不能带/,写成path:'/home/news'会被当成一级路由。 - 父组件里必须写
<Outlet />,二级路由组件才会渲染出来,否则点链接地址变了但页面没内容。 - 编程式导航的价值在于可以加判断条件(如 token 校验),这是
<Link>做不到的;navigate(url)默认新增历史记录,{replace:true}才替换当前记录;navigate(-1)/navigate(1)是前进后退。 useNavigate()只能在组件函数体顶层调用,别写在if或普通函数外面。
示例三:路由传参(path / query / state)+ 路由懒加载
jsx
// src/routes/index.js
import React, { lazy, Suspense } from 'react'
import { Navigate } from 'react-router-dom'
import Home from '../pages/Home'
import Message from '../pages/Message'
import PageNotFound from '../pages/PageNotFound'
// 1. 使用 lazy 函数加载路由组件(按需加载)
const PathTest = lazy(() => import('../pages/PathTest'))
const QueryTest = lazy(() => import('../pages/QueryTest'))
const StateTest = lazy(() => import('../pages/StateTest'))
// 2. 使用 Suspense 包裹路由组件,fallback 是未加载完成时显示的内容
function load(Com) {
return (
<Suspense fallback={<div>组件正在加载中....</div>}>
<Com />
</Suspense>
)
}
const routes = [
{
path: '/home',
element: <Home />,
children: [
{ path: 'message', element: <Message /> },
// path-params 参数需要配置路径占位符
{ path: 'message/path/:id/:school', element: load(PathTest) },
// query 参数无需配置参数占位符
{ path: 'message/query', element: load(QueryTest) },
// state 参数无需特殊配置
{ path: 'message/state', element: load(StateTest) }
]
},
{ index: true, element: <Navigate to='/home' /> },
{ path: '*', element: <PageNotFound /> }
]
export default routesjsx
// src/pages/Message.jsx
import React from 'react'
import { Link } from 'react-router-dom'
export default function Message() {
return (
<div>
<h4>路由传参</h4>
<ul>
<li><Link to='/home/message/path/1/atguigu'>path参数的传递和接收</Link></li>
<li><Link to="/home/message/query?username=atguigu&age=20">query参数的传递</Link></li>
<li>
<Link to='/home/message/state' state={{ id: 1, title: '学习路由', isDone: false }}>
state参数
</Link>
</li>
</ul>
</div>
)
}jsx
// src/pages/PathTest.jsx
import React from 'react'
import { useParams } from 'react-router-dom'
export default function PathTest() {
// 解构接收 path 参数
let { id, school } = useParams()
return <div>id: {id},school: {school}</div>
}
// src/pages/QueryTest.jsx
import React from 'react'
import { useSearchParams } from 'react-router-dom'
export default function QueryTest() {
let [searchQuery, setSearchQuery] = useSearchParams()
let username = searchQuery.get('username')
let age = searchQuery.get('age')
return (
<div>
<p>username: {username}</p>
<p>age: {age}</p>
<button onClick={() => setSearchQuery('?username=尚硅谷&age=100')}>
修改query参数
</button>
</div>
)
}
// src/pages/StateTest.jsx
import React from 'react'
import { useLocation } from 'react-router-dom'
export default function StateTest() {
let { state: { id, title, isDone } } = useLocation()
return <div>id: {id},title: {title},isDone: {String(isDone)}</div>
}关键行与易错点
lazy(() => import('路径'))的返回值必须用<Suspense fallback={...}>包起来,否则组件加载中会报错;load(Com)就是为了复用这段包裹逻辑。- path 参数必须先在路由
path里用:id、:school声明占位符,再用useParams()接收;链接里写/home/message/path/1/atguigu与占位符个数必须一一对应。 - query 参数路由不需要配置占位符,用
useSearchParams()获取,取值必须用.get('key'),不能直接当对象属性点;修改参数用返回的setSearchQuery。 - state 参数通过
<Link state={数据}>传递,可以传复杂数据类型,且不会出现在地址栏;接收用useLocation()解构state,直接刷新页面 state 可能丢失,需要判空。 useParams/useSearchParams/useLocation都是 Hook,只能在路由组件内部顶层调用。
常见错误
现象:控制台报
useRoutes() may be used only in the context of a <Router>,或 NavLink 一点击就抛错。 原因:根组件没有被BrowserRouter包裹,或包裹位置放错(包在了某个子组件里,兄弟/祖先组件用不到路由上下文)。 修法:在index.js中用<BrowserRouter><App/></BrowserRouter>包一次,且全项目只包这一处。现象:点导航整页刷新,地址变了但页面闪一下、状态全丢。 原因:用了原生
<a href>,或把Link的属性写成了href。 修法:改用<Link to="...">/<NavLink to="...">,跳转地址一律走to。现象:二级路由能点、地址也变,但父页面内容消失或子内容不显示。 原因:父路由组件里没写
<Outlet/>;或 children 的 path 写成/home/news这种绝对路径,被当成一级路由。 修法:父组件留<Outlet/>占位;children 的 path 用相对写法news。现象:访问不存在的地址页面全白;或反过来,所有地址都显示 404 页。 原因:
path:'*'漏配、没放在路由表里,或放在了index之前被优先匹配。 修法:*一定放在路由表数组最后,element 指向 PageNotFound。现象:
useParams()拿到空对象,useSearchParams().get('x')返回 null。 原因:path-params 忘了在 path 里写:id占位符;query 参数却用useParams去取。 修法:path 配'/home/message/path/:id/:school'再用useParams()解构;query 用useSearchParams()的get。现象:懒加载路由白屏,或报 "A component suspended while responding to synchronous input"。 原因:
lazy()出来的组件没有被Suspense包裹。 修法:封装load(Com)统一返回<Suspense fallback={...}><Com/></Suspense>,路由表里 element 一律走它。现象:NavLink 自定义高亮失效,DOM 上 className 是
undefined。 原因:className 回调函数没写 return,或返回了 undefined;或自定义类名后忘了写对应 CSS。 修法:className={({isActive}) => isActive ? 'myselfActive' : ''},并补上.myselfActive样式。现象:
navigate('/user')写在组件函数体里,报 "Cannot update while rendering" 或无限跳转。 原因:在 render 阶段直接调用导航。 修法:放到事件回调、useEffect或条件分支里的副作用位置调用。
练习
- 把 Login / Home / User 三个页面改造成路由表 + useRoutes 的写法,单独建
src/routes/index.js,并补上index重定向与*兜底。 - 在
/home下搭二级路由news、message,分别用 path 参数、query 参数、state 参数传一组数据,并在子组件里取出console.log验证(刷新后观察哪些参数还在)。 - 给导航栏 NavLink 写自定义高亮类名,用 CSS 实现高亮;同时用开发者工具对比 NavLink 与 Link 生成的真实 DOM,说明差在哪。
- 把 About / Message / PathTest 改成懒加载,用统一的
load()包 Suspense,在浏览器 Network 面板限速后观察 fallback 出现时机。 - 用
useNavigate实现:未登录访问/user自动跳/login,登录成功用replace跳回;再加一个"返回上一页"按钮,要求不产生多余历史记录。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 前端路由的理解 | 07:51 |
| 2 | react-路由基本概念及使用逻辑 | 10:10 |
| 3 | react-路由基本使用 | 08:55 |
| 4 | Navigate重定向 | 05:22 |
| 5 | 404页面配置 | 04:16 |
| 6 | NavLink-Link-实现无刷新跳转-active高亮 | 08:26 |
小结
- 一切从
BrowserRouter开始:容器只有一处,位置错会让后面所有报错看起来毫不相关。 - 排错按顺序走:地址栏 URL → 路由表 path → element 组件 → 组件内 Outlet / 参数 hook,逐层确认比盲改快得多。
- 一级路由写绝对路径、二级路由写相对路径,是避免 path 冲突最省心的约定。
- 重定向用
Navigate、兜底用*,两者都收进路由表统一管理,不要散落在各组件 JSX 里。 - 传参选型:能放 URL 的走 path / query(可刷新、可分享),敏感或复杂对象才用 state。
- NavLink 只在需要高亮时用,普通跳转一律 Link,少一层 class 计算也少一类 bug。
- 懒加载的 Suspense 包裹应封装成公共函数,避免每个 element 重复手写。
- 上线前确认 history 模式需服务端把未知路径 fallback 到
index.html,否则刷新即 404;可先用 HashRouter 验证业务逻辑。