Skip to content

其他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-dom
jsx
// 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>

为什么需要:用户访问网站根目录 / 时,通常希望看到首页,而不是空白。怎么想:不要给 Home 同时配两个路径,而是访问 / 时用 Navigate 重定向到 /home,保持一个 URL 对应一个路由组件的清晰关系。怎么做:在根路径的 element 中放 <Navigate to="/home" />。转写稿还给出等价写法:index={true},以及值为真时可省略成 index。调试时刷新根路径,会自动跳到 /home 并展示 Home。易错点:Navigate 的跳转地址属性是 to,不是 href;三种写法不要同时启用,选一种即可。

404 页面与通配符 ​

为什么需要:用户可能输错地址,访问不存在的路径;没有匹配时页面空白会让人困惑。怎么想:创建一个 NotFound 路由组件,再用通配符路径接住所有未匹配地址。怎么做:配置 <Route path="*" element={<NotFound />} />。调试时访问不存在路径显示 404,访问已配置路径仍显示对应组件。易错点:* 是通配符,表示匹配范围很广;路由配置中放在末尾更直观。转写稿还联系正则:* 零个或多个,+ 一个或多个,? 零个或一个,别把含义记混。

为什么需要:普通 <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 用 * 通配兜底。

为什么需要:普通 <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,二者不是同一个概念。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

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 routes
jsx
// 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 routes
jsx
// 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,只能在路由组件内部顶层调用。

常见错误 ​

  1. 现象:控制台报 useRoutes() may be used only in the context of a <Router>,或 NavLink 一点击就抛错。 原因:根组件没有被 BrowserRouter 包裹,或包裹位置放错(包在了某个子组件里,兄弟/祖先组件用不到路由上下文)。 修法:在 index.js 中用 <BrowserRouter><App/></BrowserRouter> 包一次,且全项目只包这一处。

  2. 现象:点导航整页刷新,地址变了但页面闪一下、状态全丢。 原因:用了原生 <a href>,或把 Link 的属性写成了 href。 修法:改用 <Link to="..."> / <NavLink to="...">,跳转地址一律走 to。

  3. 现象:二级路由能点、地址也变,但父页面内容消失或子内容不显示。 原因:父路由组件里没写 <Outlet/>;或 children 的 path 写成 /home/news 这种绝对路径,被当成一级路由。 修法:父组件留 <Outlet/> 占位;children 的 path 用相对写法 news。

  4. 现象:访问不存在的地址页面全白;或反过来,所有地址都显示 404 页。 原因:path:'*' 漏配、没放在路由表里,或放在了 index 之前被优先匹配。 修法:* 一定放在路由表数组最后,element 指向 PageNotFound。

  5. 现象:useParams() 拿到空对象,useSearchParams().get('x') 返回 null。 原因:path-params 忘了在 path 里写 :id 占位符;query 参数却用 useParams 去取。 修法:path 配 '/home/message/path/:id/:school' 再用 useParams() 解构;query 用 useSearchParams() 的 get。

  6. 现象:懒加载路由白屏,或报 "A component suspended while responding to synchronous input"。 原因:lazy() 出来的组件没有被 Suspense 包裹。 修法:封装 load(Com) 统一返回 <Suspense fallback={...}><Com/></Suspense>,路由表里 element 一律走它。

  7. 现象:NavLink 自定义高亮失效,DOM 上 className 是 undefined。 原因:className 回调函数没写 return,或返回了 undefined;或自定义类名后忘了写对应 CSS。 修法:className={({isActive}) => isActive ? 'myselfActive' : ''},并补上 .myselfActive 样式。

  8. 现象:navigate('/user') 写在组件函数体里,报 "Cannot update while rendering" 或无限跳转。 原因:在 render 阶段直接调用导航。 修法:放到事件回调、useEffect 或条件分支里的副作用位置调用。

练习 ​

  1. 把 Login / Home / User 三个页面改造成路由表 + useRoutes 的写法,单独建 src/routes/index.js,并补上 index 重定向与 * 兜底。
  2. 在 /home 下搭二级路由 news、message,分别用 path 参数、query 参数、state 参数传一组数据,并在子组件里取出 console.log 验证(刷新后观察哪些参数还在)。
  3. 给导航栏 NavLink 写自定义高亮类名,用 CSS 实现高亮;同时用开发者工具对比 NavLink 与 Link 生成的真实 DOM,说明差在哪。
  4. 把 About / Message / PathTest 改成懒加载,用统一的 load() 包 Suspense,在浏览器 Network 面板限速后观察 fallback 出现时机。
  5. 用 useNavigate 实现:未登录访问 /user 自动跳 /login,登录成功用 replace 跳回;再加一个"返回上一页"按钮,要求不产生多余历史记录。

原视频对照 ​

#视频时长
1前端路由的理解07:51
2react-路由基本概念及使用逻辑10:10
3react-路由基本使用08:55
4Navigate重定向05:22
5404页面配置04:16
6NavLink-Link-实现无刷新跳转-active高亮08:26

小结 ​

  1. 一切从 BrowserRouter 开始:容器只有一处,位置错会让后面所有报错看起来毫不相关。
  2. 排错按顺序走:地址栏 URL → 路由表 path → element 组件 → 组件内 Outlet / 参数 hook,逐层确认比盲改快得多。
  3. 一级路由写绝对路径、二级路由写相对路径,是避免 path 冲突最省心的约定。
  4. 重定向用 Navigate、兜底用 *,两者都收进路由表统一管理,不要散落在各组件 JSX 里。
  5. 传参选型:能放 URL 的走 path / query(可刷新、可分享),敏感或复杂对象才用 state。
  6. NavLink 只在需要高亮时用,普通跳转一律 Link,少一层 class 计算也少一类 bug。
  7. 懒加载的 Suspense 包裹应封装成公共函数,避免每个 element 重复手写。
  8. 上线前确认 history 模式需服务端把未知路径 fallback 到 index.html,否则刷新即 404;可先用 HashRouter 验证业务逻辑。