react(五)路由

文章目录

React Router 是 React 官方推荐的路由解决方案,用于实现单页应用(SPA)中的页面导航------不刷新页面就能切换显示不同组件,同时管理浏览器历史记录和URL。

一、为什么需要路由?

传统网页点击链接会向服务器请求新页面,整个页面重新加载。而 SPA(单页应用) 只有一个 index.html,需要前端自己控制"显示哪个组件"以及"URL 是什么"------这就是路由要做的事情。

二、安装

bash 复制代码
npm install react-router-dom

react-router-dom 是 Web 开发使用的包,已内置 TypeScript 类型定义。

三、两种路由配置方式

  1. 编程式配置 - 使用 createBrowserRouter + RouterProvider(对象数组 + useRoutes)(推荐)
  • 特点:结合 React Router 与类似 Remix 的框架能力(如数据加载、变异、重新验证)。
  • 典型 API:createBrowserRouter、RouterProvider、loader、action。
  • 适用场景:大型应用,需要嵌套路由、数据预加载、错误边界、代码分割等企业级特性。
  • 优点:数据获取与路由同步,更好的用户体验(如导航时保持数据)。
  • 缺点:学习曲线较陡,需要组织服务端或构建时数据。
js 复制代码
// routes.js
import { createBrowserRouter } from 'react-router-dom'
import { HomePage, AboutPage, ErrorPage } from '../components/15.routeView';

export const routes = createBrowserRouter([
  {
    // 路由路径 - 匹配URL的路径模式
    path: '/',
    // element: 指定要渲染的组件
    element: <HomePage />,
    // errorElement: 指定当路由匹配失败时显示的组件
    errorElement: <ErrorPage />,
  },
  {
    path: '/about',
    element: <AboutPage />
    // 注意:这个路由没有配置 errorElement,会向上查找使用根路由的errorElement(错误边界)
  }
]);
js 复制代码
// main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import { RouterProvider } from 'react-router-dom'
import { routes } from './routes' // 上面的routes.js文件

createRoot(document.getElementById('root')!).render(
  <StrictMode>
  	// RouterProvider:接收创建好的 routes 实例,自动处理路由渲染
    <RouterProvider router={routes} />
  </StrictMode>
)

createBrowserRouter 参数数组中的路由对象属性:

  • path:string,必填,URL路径模式,匹配浏览器地址栏的路径
  • element:React.ReactNode,必填,路径匹配时渲染的React组件
  • errorElement:React.ReactNode,选填,路由渲染出错或数据加载失败时显示的备用组件

RouterProvider 组件属性

  • router:Router,必填,createBrowserRouter创建的路由实例
  • fallbackElement:React.ReactNode,选填,路由懒加载时的加载中组件
js 复制代码
<RouterProvider 
  router={router}
  fallbackElement={<LoadingSpinner />}  // 可选:懒加载时的loading界面
/>
  1. 声明式模式 - 使用 + (JSX)
  • 特点:组件式路由,类似于 React Router v5 及之前的主流风格。
  • 典型 API:、、、。
  • 适用场景:小型到中型应用,原型开发,或对简单性有高要求的项目。
  • 优点:上手快,组件化程度高,与 React 心智模型一致。
  • 缺点:不利于复杂的数据依赖和嵌套布局优化。
js 复制代码
// App.tsx
import './App.css'
import { HomePage, AboutPage, ErrorPage } from './components/15.routeView';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
  	// BrowserRouter:路由容器组件,为应用提供基于浏览器 history API 的路由功能
      <BrowserRouter>
      // Routes: 路由的容器,它会检查其所有的 <Route>子元素,找出与当前 URL 最匹配的那个来渲染
        <Routes>
        // Route: 定义单个路由规则
          <Route path="/" element={<HomePage />}  />
          <Route path="/about" element={<AboutPage />} />
          // path="*"是一个通配符路由,用于匹配所有未被前面 Route 精确匹配的 URL。
          // 一般放在最后面,React Router 从上到下匹配,如果 *写在前面
          // 那么 所有路径都会先被 *拦截,导致永远显示 ErrorPage。
          <Route path="*" element={<ErrorPage />} />
        </Routes>
      </BrowserRouter>
  )
}

export default App

四、嵌套路由

嵌套路由是指在父路由对应的组件内部,再渲染子路由对应的组件。就像俄罗斯套娃一样,一个页面里面可以包含另一个页面。

实际例子:

一个后台管理系统:顶部和侧边栏是固定的(父路由),右侧内容区域根据点击切换(子路由)

一个产品页面:左侧是产品列表(父路由的一部分),点击某个产品后,右侧显示产品详情(子路由)

基础嵌套路由

实现步骤

  1. 创建父路由组件(布局组件)

父组件需要做两件事:

  • 渲染公共部分(导航栏、侧边栏等)
  • 放置 作为子路由的渲染位置
    Outlet: 是占位符组件,告诉 React Router 在哪里渲染嵌套的子路由组件
js 复制代码
import { Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <header>顶部导航(始终显示)</header>
      <Outlet />  {/* 占位符,这里就是子路由组件内容要显示的地方 */}
      <footer>底部版权(始终显示)</footer>
    </div>
  );
}
  1. 在路由配置中定义嵌套关系
    使用 嵌套的 <Route> 来定义父子关系:
  • 声明式写法
html 复制代码
<Route path="/" element={<Layout />}>   {/* 父路由 */}
	{/* 相对路径:子路由不需要写完整路径,会继承父路由的路径 */}
  <Route path="about" element={<About />} />    {/* 子路由,路径会自动拼接为 /about */}
  <Route path="contact" element={<Contact />} /> {/* 子路由 */}
</Route>
  • 编程式写法
js 复制代码
import { createBrowserRouter } from 'react-router-dom'
import { Layout } from '../components/16.mulRoute/Layout ';
import { Contact } from '../components/16.mulRoute/contact';
import { About } from '../components/16.mulRoute/about';

export const routes = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    // children 为子路由数组,对象属性和父路由的一样
    children: [
      { path: 'about', element: <About /> },
      { path: 'contact', element: <Contact /> }
    ]
  }
]);

访问规则:

  • 访问 /about → 显示 Layout + About 组件,中间about为子路由的内容

  • 访问 /contact → 显示 Layout + Contact 组件,中间contact为子路由的内容

  • 访问 / → 显示 Layout,只有父组件内容

  1. 添加默认子路由(index 路由)
    当访问父路由本身(如 /)时,需要一个默认显示的内容:
html 复制代码
<Route path="/" element={<Layout />}>
  <Route index element={<Contact />} />  {/* index 表示默认子路由,不用写path */}
  <Route path="about" element={<About />} />
</Route>

访问 / 时 → 显示 Layout + Contact ,父组件加默认子路由内容

路由也可无限嵌套下去。

bash 复制代码
<Route path="users" element={<UsersLayout />}>      {/* 第一层 */}
  <Route path="detail" element={<UserDetail />}>   {/* 第二层 */}
    <Route path="profile" element={<Profile />} />  {/* 第三层 */}
  </Route>
</Route>
js 复制代码
export const routes = createBrowserRouter([
  {
    path: '/users', // 第一层
    element: <UsersLayout />,
    children: [ // 第二层
      { 
        path: 'detail',
        element: <UserDetail />,
        children: [  // 第三层
          { 
          	path: 'profile',
          	element: <Profile /> 
          }
        ]
      }
    ]
  }
]);

访问 /users/detail/profile 时:

  • 先渲染 UsersLayout
  • 在 UsersLayout 的 Outlet 位置渲染 UserDetail
  • 在 UserDetail 的 Outlet 位置渲染 Profile

布局路由(无路径嵌套)

使用 element 但不设置 path,可以创建纯布局路由,不贡献 URL 路径段:

bash 复制代码
<Routes>
  {/* 这个布局路由不增加 URL 路径 */}
  <Route element={<AuthLayout />}>
    <Route path="/login" element={<Login />} />
    <Route path="/register" element={<Register />} />
  </Route>
</Routes>

访问 /login 时,Login 组件会渲染在 AuthLayout 的 位置。

五、动态路由

React 动态路由的核心是通过 URL 中的参数来渲染不同内容。
动态路由就是路由路径中带有可变部分(参数),例如 /user/123、/product/456,其中 123、456 就是动态参数。

主要步骤

  1. 定义动态路由:使用 :参数名 的格式,如
js 复制代码
// :id 就是动态路由,可以传不同的数据进来
<Route path="/user/:id" element={<User />} />
  1. 获取路由参数:在组件中使用 useParams() Hook 来获取参数值
js 复制代码
import { useParams } from 'react-router-dom';
const { id } = useParams();  // 得到 URL 中的实际值
  1. 根据参数渲染:拿到参数后,通常用来请求对应的数据,然后展示不同内容

流程

用户点击链接 /user/123 → React Router 匹配到 /user/:id → 渲染 User 组件 → 组件通过 useParams() 拿到 {id: "123"} → 根据 id 请求用户数据 → 展示用户 123 的信息

使用

  1. 路由配置
js 复制代码
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        {/* 动态路由参数用 : 标识 */}
        <Route path="/user/:userId" element={<UserProfile />} />
         {/* 多个参数 */}
        <Route path="/post/:postId/comment/:commentId" element={<Comment />} />
      </Routes>
    </BrowserRouter>
  );
}
  1. 使用 useParams Hook
bash 复制代码
import { useParams } from 'react-router-dom';

function UserProfile() {
  // 获取路由参数
  const { userId } = useParams();
  
  return (
    <div>
      <h1>用户ID: {userId}</h1>
    </div>
  );
}

// 多个参数
function Comment() {
  const { postId, commentId } = useParams();
  
  return (
    <div>
      <p>文章ID: {postId}</p>
      <p>评论ID: {commentId}</p>
    </div>
  );
}
  1. 使用路由参数获取数据
js 复制代码
import { useParams, useNavigate } from 'react-router-dom';
import { useState, useEffect } from 'react';

function UserProfile() {
  const { userId } = useParams();
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  // 导航
  const navigate = useNavigate();

  useEffect(() => {
    // 根据路由参数获取数据
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('获取用户失败:', error);
        setLoading(false);
      });
  }, [userId]); // userId变化时重新获取数据

  if (loading) return <div>加载中...</div>;
  if (!user) return <div>用户不存在</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>邮箱: {user.email}</p>
      <button onClick={() => navigate('/users')}>返回列表</button>
    </div>
  );
}

注意事项

  • 参数变化时组件会重新渲染,需要重新获取数据
  • 多个参数可以共存,如 /post/:postId/comment/:commentId
  • 参数默认是字符串类型,数字需要手动转换

简单说就是:URL 留个变量位置,组件拿到变量值,根据值显示对应内容。

六、可选参数

可选参数就是 URL 中可以有也可以没有的参数,让同一个路由能匹配多种 URL 模式。

基本语法

React Router v6 中使用 ? 号标记参数为可选

js 复制代码
// 定义可选参数
<Route path="/page/:pageNum?" element={<Page />} />

匹配规则

假设定义路由:/user/:id?

  • 访问 /user/123 → 匹配成功,id = "123"
  • 访问 /user → 匹配成功,id = undefined
  • 访问 /user/123/profile → 不匹配(路径层级不同)

实例

js 复制代码
// 商品列表页,支持可选的分类参数
function ProductList() {
  const { category } = useParams();
  
  // 没有 category 参数时显示全部商品
  const apiUrl = category 
    ? `/api/products?category=${category}`
    : '/api/products';
    
  // ... 请求数据并渲染
}

// 路由配置
<Route path="/products/:category?" element={<ProductList />} />

// 使用方式
// /products        → 显示全部商品
// /products/手机    → 只显示手机分类

注意点

  • 只影响同一层级:/user/:id? 不会匹配 /user/123/profile,那是不同层级
  • 可选参数必须在末尾:不能是 /user/:id?/profile,这会导致问题
  • 多个可选参数有顺序:/a/:b?/:c? 中,省略 b 就必须省略 c,不能跳过
  • 默认值处理:始终要处理参数为 undefined 的情况

七、导航组件

基础导航组件,渲染为 标签,实现无刷新页面跳转

js 复制代码
import { Link } from 'react-router';

// 基础用法
<Link to="/about">关于我们</Link>

// replace: 替换历史记录而不是新增
<Link to="/dashboard" replace>仪表板(替换模式)</Link>

// state: 传递状态数据(目标页通过 useLocation 接收)
<Link to="/profile" state={{ from: 'homepage' }}>个人资料</Link>

// reloadDocument: 强制整页刷新(跳过 SPA 导航)
<Link to="/old-page" reloadDocument>旧版页面</Link>

// viewTransition: 启用页面过渡动画(需要浏览器支持)
<Link to="/gallery" viewTransition>画廊</Link>

Link 的增强版,自动添加激活状态样式,支持 active、pending、transitioning 三种状态

三种状态说明:

  • isActive:当前 URL 与 to 匹配时激活
  • isPending:是否正在等待加载(用于懒加载场景)
  • isTransitioning:启用了 viewTransition 且过渡动画进行中

基本用法

js 复制代码
import { NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <NavLink to="/">首页</NavLink>
      <NavLink to="/about">关于</NavLink>
      <NavLink to="/contact">联系</NavLink>
    </nav>
  );
}

激活样式设置

方式1:使用 className 函数

html 复制代码
<NavLink 
  to="/user" 
  className={({ isActive }) => isActive ? 'active-link' : 'normal-link'}
>
  用户中心
</NavLink>

方式2:使用 style 函数

html 复制代码
<NavLink 
  to="/settings"
  style={({ isActive }) => ({
    color: isActive ? 'red' : 'black',
    fontWeight: isActive ? 'bold' : 'normal'
  })}
>
  设置
</NavLink>

方式3:使用 activeClassName(v5 语法,v6 仍支持)

html 复制代码
// v6 中仍可使用,但推荐 className 函数方式
<NavLink to="/home" activeClassName="active">
  首页
</NavLink>

NavLink 就是自带高亮功能的 Link,通过 isActive 自动判断当前路径是否匹配,你只需要告诉它激活时用什么样式(className 或 style),它就会自动切换。常用于导航栏、菜单栏、标签页等需要显示当前所在位置的地方。

八、编程式导航 Hooks

1. useNavigate

useNavigate 是 React Router v6 提供的编程式导航 Hook,让你可以在 JavaScript 代码中控制页面跳转,而不是通过 组件。

核心概念: 把导航能力变成函数调用,在任何时机、任何条件下跳转页面。

js 复制代码
// 1. 在组件中导入
import { useNavigate } from 'react-router-dom';

function MyComponent() {
	// 2. 调用 Hook 得到导航函数
  const navigate = useNavigate();
  
  return (
  	// 使用导航函数,跳转到 about 页面
    <button onClick={() => navigate('/about')}>
      去关于页面
    </button>
  );
}

关键点:

  • useNavigate() 返回一个 navigate 函数
  • navigate() 接收目标路径作为参数
  • 必须在 React 函数组件或自定义 Hook 中调用

所有使用方式

  1. 字符串路径(最常用)
js 复制代码
navigate('/about'); // 绝对路径
navigate('../contact'); // 相对路径
  1. 数字(用于前进或后退)
js 复制代码
navigate(-1); // 后退一页(相当于浏览器后退按钮)
navigate(-2); // 后退两页
navigate(1); // 前进一页
  1. 对象(更灵活,可以同时传递路径、查询参数、哈希值和状态数据)
js 复制代码
navigate({
  pathname: '/about', // 路径
  search: '?category=electronics&page=2', // 查询参数
  hash: '#reviews' // 哈希值
})
// 结果:/about?category=electronics&page=2#reviews

// 也可用字符串
navigate('/about?category=electronics&page=2#reviews'); // 等效于上面
  1. replace: true(替换当前历史记录条目,而不是添加新条目)
    作用: 不在历史记录中留下当前页面,用户点击后退不会回到这个页面
    使用场景: 登录页 → 首页,不希望用户后退又回到登录页
js 复制代码
navigate('/about', { replace: true });
  1. 传递状态数据
js 复制代码
// 发送方
navigate('/result', { 
  state: { 
    score: 95,
    userName: '张三',
    answers: [1,2,3,4]
  }
})
js 复制代码
// 接收方(在目标组件中)
import { useLocation } from 'react-router-dom';

function ResultPage() {
  const location = useLocation();
  const data = location.state;  // { score: 95, userName: '张三', answers: [...] }
  
  return <div>得分:{data.score}</div>;
}

使用场景

  1. 表单提交后跳转(最常见)
js 复制代码
export const ActionFrom = () => {
  const submitForm = async (formData: FormData) => {
    try {
      // 
      const response = await mockPostRequest(formData);
      if (response.success) {
        // 登录成功,跳转到仪表盘
        navigate('/dashboard');
      } else {
        // 登录失败,跳转到错误页并传递错误信息
        navigate('/error', { 
          state: { message: response.error } 
        });
      }
    } catch (error) {
      console.error('提交表单时发生错误:', error);
    }
  }
  return (
    <div>
      <form action={submitForm}>
        <div>
          <label htmlFor="name">姓名:</label>
           <input name="name" placeholder="姓名" />
        </div>
        <div>
          <label htmlFor="email">邮箱:</label>
          <input name="email" placeholder="邮箱" />
        </div>
      	<button type="submit">提交</button>
      </form>
    </div>
  )
}
  1. 条件权限控制(配合 useEffect)
js 复制代码
function AdminPage() {
  const navigate = useNavigate();
  const user = useSelector(state => state.user);  // 假设从 Redux 获取用户
  
  useEffect(() => {
    // 页面加载时检查权限
    if (!user.isLoggedIn) {
      navigate('/login', { replace: true });
      return;
    }
    
    if (user.role !== 'admin') {
      navigate('/unauthorized', { replace: true });
      return;
    }
  }, [user, navigate]);  // 依赖项变化时重新检查
  
  return <div>管理员页面内容</div>;
}
  1. 搜索后跳转并传参
js 复制代码
function SearchBar() {
  const navigate = useNavigate();
  
  const handleSearch = (keyword, category) => {
    // 方式1:URL 参数
    navigate(`/search?q=${keyword}&cat=${category}`);
    
    // 方式2:使用对象形式(更清晰)
    navigate({
      pathname: '/search',
      search: `?q=${encodeURIComponent(keyword)}&cat=${category}`
    });
  };
  
  return (
    <input 
      type="text" 
      onKeyPress={(e) => {
        if (e.key === 'Enter') {
          handleSearch(e.target.value, 'books');
        }
      }}
    />
  );
}

// 在搜索结果页读取参数
function SearchResults() {
  const [searchParams] = useSearchParams();  // 另一个 Hook
  const keyword = searchParams.get('q');
  const category = searchParams.get('cat');
  
  return <div>搜索 "{keyword}" 的结果</div>;
}

2. useLocation

useLocation 是 React Router v6 提供的 URL 信息 Hook,让你可以实时获取当前页面的完整 URL 信息,包括路径、查询参数、哈希值和通过 navigate 传递的状态数据。

核心概念: 把浏览器的地址栏信息变成 React 可响应的数据,当 URL 变化时组件会自动重新渲染。

js 复制代码
import { useLocation } from 'react-router';

function CurrentPage() {
  const location = useLocation();
  
  console.log(location);  // 查看当前 URL 的所有信息
  console.log(location.pathname);  // 当前路径,如 "/user/123"
  console.log(location.search);    // 查询字符串,如 "?tab=profile"
  console.log(location.hash);      // 哈希值,页面内锚点定位,如 "#section"
  console.log(location.state);     // 通过 navigate 传递的状态数据
  
  return <div>当前页面:{location.pathname}</div>;
}

3. useSearchParams

useSearchParams 是 React Router v6 提供的专门用于操作 URL 查询参数的 Hook,它让你可以像使用 useState 一样方便地读取和修改 URL 中 ? 后面的参数。

核心概念: 把 URL 查询字符串变成响应式的状态,修改它会自动更新浏览器地址栏,并且组件会重新渲染。

js 复制代码
import { useSearchParams } from 'react-router-dom';

function SearchComponent() {
  const [searchParams, setSearchParams] = useSearchParams();
  
  // searchParams 是一个 URLSearchParams 对象
  // setSearchParams 是一个函数,用来修改参数
  
  return <div>当前参数数量:{searchParams.size}</div>;
}

searchParams 对象的常用方法

js 复制代码
const [searchParams, setSearchParams] = useSearchParams();

// 假设 URL: /products?category=电子&page=2&sort=price

// 1. 获取单个参数
const category = searchParams.get('category');     // '电子'
const page = searchParams.get('page');             // '2'
const sort = searchParams.get('sort');             // 'price'
const none = searchParams.get('none');             // null

// 2. 获取所有参数(返回迭代器)
const allParams = Object.fromEntries(searchParams.entries());
// 结果:{ category: '电子', page: '2', sort: 'price' }

// 3. 检查参数是否存在
const hasCategory = searchParams.has('category');  // true

// 4. 获取同一参数的多个值(不常用)
// URL: /search?tag=react&tag=vue&tag=angular
const tags = searchParams.getAll('tag');  // ['react', 'vue', 'angular']

// 5. 获取所有参数名
const keys = [...searchParams.keys()];    // ['category', 'page', 'sort']

// 6. 获取所有参数值
const values = [...searchParams.values()]; // ['电子', '2', 'price']

// 7. 遍历所有参数
searchParams.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});

修改查询参数的方法

js 复制代码
const [searchParams, setSearchParams] = useSearchParams();

// 方式1:传入对象,整个参数对象都更新
setSearchParams({ page: '3', sort: 'desc' });
// URL 变成:?page=3&sort=desc

// 方式2:传入 URLSearchParams 对象
const newParams = new URLSearchParams();
newParams.set('page', '3');
newParams.set('size', '20');
setSearchParams(newParams);

// 方式3:函数式更新(基于当前参数),保留其他现有参数
setSearchParams(prev => {
  prev.set('page', '3');
  return prev;
});

// 方式4:替换历史(不产生历史记录)
setSearchParams({ page: '3' }, { replace: true });

// 删除单个参数
// delete(key) 不能直接调用,必须在 setSearchParams 的回调函数中使用,因为 searchParams 是只读的。
setSearchParams(prev => {
   prev.delete('page'); 
   return prev;
});

// 清空所有参数
setSearchParams({});

使用场景

  1. 搜索/筛选功能(最重要)
js 复制代码
function ProductSearch() {
  const [searchParams, setSearchParams] = useSearchParams();
  
  // 从 URL 读取参数
  const keyword = searchParams.get('keyword') || '';
  const category = searchParams.get('category') || 'all';
  const minPrice = searchParams.get('minPrice') || '';
  const maxPrice = searchParams.get('maxPrice') || '';
  const inStock = searchParams.get('inStock') === 'true';
  
  const handleSearch = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    
    // 构建新的查询参数(只传有值的)
    const params = {};
    if (formData.get('keyword')) params.keyword = formData.get('keyword');
    if (formData.get('category') !== 'all') params.category = formData.get('category');
    if (formData.get('minPrice')) params.minPrice = formData.get('minPrice');
    if (formData.get('maxPrice')) params.maxPrice = formData.get('maxPrice');
    if (formData.get('inStock')) params.inStock = 'true';
    
    setSearchParams(params);
  };
  
  const clearFilters = () => {
    setSearchParams({});  // 清空所有参数
  };
  
  return (
    <div>
      <form onSubmit={handleSearch}>
        <input name="keyword" defaultValue={keyword} placeholder="搜索商品" />
        
        <select name="category" defaultValue={category}>
          <option value="all">全部分类</option>
          <option value="电子">电子</option>
          <option value="服装">服装</option>
        </select>
        
        <input name="minPrice" defaultValue={minPrice} placeholder="最低价" />
        <input name="maxPrice" defaultValue={maxPrice} placeholder="最高价" />
        
        <label>
          <input type="checkbox" name="inStock" defaultChecked={inStock} />
          仅显示有货
        </label>
        
        <button type="submit">搜索</button>
        <button type="button" onClick={clearFilters}>清空筛选</button>
      </form>
      
      <div>
        当前筛选条件:
        {keyword && <span>关键词:{keyword}</span>}
        {category !== 'all' && <span>分类:{category}</span>}
        {minPrice && <span>最低价:{minPrice}</span>}
        {maxPrice && <span>最高价:{maxPrice}</span>}
        {inStock && <span>仅显示有货</span>}
      </div>
    </div>
  );
}
  1. 监听参数变化并加载数据
js 复制代码
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';

function ProductList() {
  const [searchParams] = useSearchParams();
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);
  
  // 监听参数变化,自动重新加载数据
  useEffect(() => {
    // 获取所有参数
    const params = Object.fromEntries(searchParams.entries());
    
    const fetchProducts = async () => {
      setLoading(true);
      try {
        // 构建 API 请求 URL
        const queryString = new URLSearchParams(params).toString();
        const response = await fetch(`/api/products?${queryString}`);
        const data = await response.json();
        setProducts(data);
      } catch (error) {
        console.error('加载失败', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchProducts();
  }, [searchParams]);  // searchParams 变化时重新请求
  
  if (loading) return <div>加载中...</div>;
  
  return (
    <div>
      <div>共 {products.length} 件商品</div>
      {/* 渲染商品列表 */}
    </div>
  );
}

九、Loader 数据加载器

Loader 是 React Router 数据路由中的数据加载器,它是一个异步函数,在路由组件渲染之前执行,负责获取页面所需的数据。

核心思想: 把数据获取的逻辑从组件中抽离出来,放到路由配置里,让路由器提前知道需要什么数据。

传统方式:组件渲染后才开始请求数据

Loader 方式:渲染前就拿到数据

Loader 的基本用法

定义 Loader

js 复制代码
import { createBrowserRouter } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/user/:userId',
    // Loader 函数
    loader: async ({ params, request }) => {
      // params: 路由参数 { userId: '123' }
      // request: 原始请求对象
      
      const user = await fetchUser(params.userId);
      return user;  // 返回数据
    },
    Component: UserProfile,
  },
]);

使用 useLoaderData

js 复制代码
import { useLoaderData } from 'react-router-dom';

function UserProfile() {
  const user = useLoaderData();  // 获取 loader 返回的数据
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>邮箱:{user.email}</p>
    </div>
  );
}

Loader 的参数详解

loader: async ({ params, request, context }) => {}

Loader 函数接收一个对象,包含以下属性:

  • params: 动态路由参数
  • request: 原始的 Fetch Request 对象
  • context: 自定义上下文(从 dataStrategy 传递)

params - 路由参数

js 复制代码
// 路由配置
{
  path: '/user/:userId/post/:postId',
  loader: ({ params }) => {
    console.log(params);
    // { userId: '123', postId: '456' }
    
    return fetch(`/api/users/${params.userId}/posts/${params.postId}`);
  }
}

request - 请求对象

js 复制代码
{
  path: '/products',
  loader: ({ request }) => {
    // 获取 URL 查询参数
    const url = new URL(request.url);
    const category = url.searchParams.get('category');
    const page = url.searchParams.get('page');
    
    // 获取请求头
    const authToken = request.headers.get('Authorization');
    
    return fetch(`/api/products?cat=${category}&page=${page}`);
  }
}

返回不同类型的数据

返回普通对象

js 复制代码
loader: async () => {
  const user = await fetchUser();
  const posts = await fetchPosts();
  
  // 返回多个数据
  return {
    user: user,
    posts: posts,
    timestamp: Date.now()
  };
}

function Component() {
  const { user, posts, timestamp } = useLoaderData();
  // ...
}

返回 Response 对象

js 复制代码
loader: async ({ params }) => {
  const response = await fetch(`/api/user/${params.id}`);
  
  if (!response.ok) {
    // 抛出错误,会被 errorElement 捕获
    throw new Response('用户不存在', { status: 404 });
  }
  
  return response;  // 直接返回 Response
}

function Component() {
  // useLoaderData 会自动解析 JSON
  const user = useLoaderData();
}

使用 redirect 跳转

js 复制代码
import { redirect } from 'react-router-dom';

loader: async ({ request }) => {
  const user = await getCurrentUser();
  
  // 未登录,跳转到登录页
  if (!user) {
    // 保存当前路径,登录后可以返回
    const url = new URL(request.url);
    return redirect(`/login?redirectTo=${url.pathname}`);
  }
  
  return user;
}

抛出错误

js 复制代码
loader: async ({ params }) => {
  try {
    const data = await fetchData(params.id);
    return data;
  } catch (error) {
    // 方式1:抛出 Response
    throw new Response('数据加载失败', { status: 500 });
    
    // 方式2:抛出 Error
    throw new Error('网络错误,请稍后重试');
    
    // 方式3:自定义错误对象
    throw {
      status: 404,
      message: '资源不存在',
      details: { id: params.id }
    };
  }
}

使用useLoaderData ,获取loader的数据

基本使用

js 复制代码
import { useLoaderData } from 'react-router-dom';

function MyComponent() {
  const data = useLoaderData();
  // data 的类型就是 loader 返回的类型
  return <div>{JSON.stringify(data)}</div>;
}

嵌套路由中的 useLoaderData

js 复制代码
const router = createBrowserRouter([
  {
    path: '/dashboard',
    loader: async () => {
      return { user: 'Alice', role: 'admin' };
    },
    Component: DashboardLayout,
    children: [
      {
        path: 'stats',
        loader: async () => {
          return { views: 1000, likes: 500 };
        },
        Component: StatsPage,
      },
    ],
  },
]);

function DashboardLayout() {
  const parentData = useLoaderData();  // { user: 'Alice', role: 'admin' }
  return (
    <div>
      <h1>欢迎 {parentData.user}</h1>
      <Outlet />  {/* 子路由渲染位置 */}
    </div>
  );
}

function StatsPage() {
  const childData = useLoaderData();   // { views: 1000, likes: 500 }
  // 注意:这里拿不到父级的数据,只能拿到自己的
  return <div>浏览量:{childData.views}</div>;
}

十、action

React Router 的 action是 Data Router(v6.4+)的核心功能,专门用于处理表单提交、删除等数据变更(Mutation)操作。它通常与 组件配合,能自动处理请求序列化,并在执行后触发数据的重新验证(Revalidation)。

你可以这样理解:

Loader:负责"读"数据(GET 请求)

Action:负责"写"数据(POST、PUT、PATCH、DELETE)

配置步骤

  1. 定义 Action 函数
    Action 是一个异步函数,接收包含 request和 params的参数对象。你需要从 request中解析表单数据,执行逻辑(如调用 API),最后返回重定向或数据。
js 复制代码
// src/routes/contact-action.js
import { redirect } from 'react-router-dom';

export async function contactAction({ request, params }) {
  // 1. 获取表单数据
  const formData = await request.formData();
  const updates = Object.fromEntries(formData);

  // 2. 执行数据变更(如更新联系人)
  await updateContact(params.contactId, updates);

  // 3. 返回重定向或数据
  return redirect(`/contacts/${params.contactId}`);
}
  1. 在路由配置中挂载
    用 createBrowserRouter创建路由时,在对应的路由对象中设置 action属性。
js 复制代码
// src/main.jsx
import { createBrowserRouter } from 'react-router-dom';
import ContactPage, { contactAction } from './routes/contact-page';

const router = createBrowserRouter([
  {
    path: '/contacts/:contactId/edit',
    element: <ContactPage />,
    // 挂载 action
    action: contactAction, 
    // 通常与 loader(数据加载)配对使用
    // loader: contactLoader,
  },
]);
  1. 在组件中使用 提交
    在组件中必须使用 React Router 提供的 组件(非原生 ),它会被自动拦截并发送到路由的 action。
js 复制代码
// src/routes/contact-page.jsx
import { Form, useLoaderData } from 'react-router-dom';

export default function ContactPage() {
  return (
    <Form method="post">
      <input type="text" name="firstName" defaultValue="John" />
      <input type="text" name="lastName" defaultValue="Doe" />
      <button type="submit">保存</button>
    </Form>
  );
}

进阶用法

  1. 获取 Action 执行结果
    Action 返回的数据(如错误信息、成功状态)可以通过 useActionDataHook 在组件中获取。
js 复制代码
import { useActionData } from 'react-router-dom';

export async function action({ request }) {
  const formData = await request.formData();
  const error = await submitForm(formData);
  // 如果有错误,返回错误对象
  if (error) return { error };
  return redirect('/success');
}

function ContactPage() {
  const actionData = useActionData(); // 获取 action 返回的数据
  return (
    <div>
      {actionData?.error && <p>错误:{actionData.error}</p>}
      {/* ... Form ... */}
    </div>
  );
}
  1. 触发方式与 Method
  • :最常用,触发路由的 action。
  • :可用于删除操作,同样会触发 action。
  • useSubmit:用于编程式触发,例如在自定义按钮点击事件中提交数据。
  1. 错误处理
    Action 中抛出的错误会被最近的路由 errorElement捕获。
js 复制代码
export async function action({ params }) {
  const contact = await deleteContact(params.contactId);
  if (!contact) {
    // 抛出错误,会被 errorElement 捕获
    throw new Response('Not Found', { status: 404 });
  }
  return redirect('/contacts');
}

// 路由配置中需配置 errorElement
{
  path: '/contacts/:contactId/destroy',
  action: destroyAction,
  errorElement: <ErrorBoundary />,
}

注意

  • 适用版本:此配置方式适用于 React Router v6.4 及以上 的 Data Router(使用 createBrowserRouter)。
  • 必须使用 <Form> :原生 的提交会触发浏览器默认行为(页面刷新),无法被 action捕获。
  • 数据重新验证:Action 执行成功后,React Router 会自动重新调用当前页面所有活跃的 loader,确保 UI 数据与后台同步(类似 SWR 效果)。

十、中间件(Middleware)v7.9.0+

从 v7.9.0 开始,React Router 正式支持中间件功能,可以在 loader 执行前后插入逻辑,如认证检查、日志记录、数据预处理等:

中间件函数的基本签名

ts 复制代码
// context - 上下文对象:包含请求相关的所有信息
interface MiddlewareContext {
  request: Request;        // 标准的Fetch API Request对象
  params: Record<string, string>; // 路由参数,如 :id
  context: ContextContainer; // 可共享数据的容器,由自己写入和使用
}

// next - 下一个函数:调用它来执行后续的中间件和路由处理器
type NextFunction = () => Promise<Response>;

type MiddlewareFunction = (context: MiddlewareContext, next: NextFunction) => Promise<Response | void>;

中间件的核心是一个接收特定参数和 next 函数的异步方法。你可以通过在不同阶段调用 next 来插入"前置"和"后置"逻辑。

ts 复制代码
// 一个典型的中间件结构
const loggingMiddleware: Route.MiddlewareFunction = async ({ request }, next) => {
  console.log(`[前置] 请求开始: ${request.url}`); // 1. 前置逻辑
  
  const response = await next(); // 2. 执行下游中间件和路由处理器
  
  console.log(`[后置] 请求完成: ${response.status}`); // 3. 后置逻辑
  return response; // 服务器端必须返回 Response
};

典型应用场景

  1. 身份验证与授权 (Authentication)
    这是中间件最核心的用例之一。通过在父级路由设置验证中间件,可以确保其所有子路由都受到保护,无需在每个loader中重复验证逻辑。如果用户未登录,中间件会直接抛出重定向,从而阻止子路由的 loader 和组件代码继续执行,避免无效操作。
ts 复制代码
// app/routes/_auth.tsx
import { redirect } from "react-router";
import { createContext } from "react-router";

// 1. 创建一个类型安全的 Context
export const userContext = createContext<User>();

// 2. 定义验证中间件
const authMiddleware: Route.MiddlewareFunction = async ({ request, context }, next) => {
  const user = await getUser(request); // 假设这是一个获取用户的函数
  if (!user) {
    // 未登录则重定向,下游的 loaders 将不会执行
    throw redirect("/login");
  }
  // 登录成功,将用户信息存入 Context 供下游使用
  context.set(userContext, user);
  
  // 调用 next(),继续执行子路由的中间件、loader 等
  await next();
};

// 3. 导出中间件数组
export const middleware = [authMiddleware];

// app/routes/_auth.profile.tsx
// context 是 loader 的标准参数,TypeScript会自动识别
export async function loader({ context }) {
  // 子路由可以安全地获取已通过验证的用户信息
  const user = context.get(userContext);
  const profileData = await fetchProfile(user.id);
  return { profile: profileData };
}
  1. 日志与监控 (Logging)
    可以方便地记录请求的耗时和状态,这对于性能分析和调试非常有用。
js 复制代码
const loggerMiddleware: Route.MiddlewareFunction = async ({ request }, next) => {
  const start = performance.now();
  // 你可以记录请求方法、URL等
  console.log(`[${request.method}] ${request.url} - 开始`);
  
  const response = await next();
  
  const duration = performance.now() - start;
  // 记录响应状态码和耗时
  console.log(`[${request.method}] ${request.url} - 完成 (${response.status}, ${duration}ms)`);
  
  return response;
};

export const middleware = [loggerMiddleware];
  1. 上下文传递 (Context API)
    React Router 提供了一个类型安全的 Context API,用于在中间件、loader 和 action 之间共享数据,替代了旧的 AppLoadContext 方式。你可以像上面认证示例那样,在中间件中设置值,然后在任何下游的 loader 或 action 中读取。
js 复制代码
// 在任意 loader 或 action 中读取 Context
export async function loader({ context }) {
  // 类型安全,user 被推断为 User 类型
  const user = context.get(userContext); 
  return { user };
}

多中间件组合语法

中间件按数组顺序执行,形成洋葱模型:

ts 复制代码
// middlewares/auth.ts
export const authMiddleware: Route.MiddlewareFunction = async (ctx, next) => {
  console.log('1. 认证中间件 - 开始');
  const response = await next(); // 👈 执行下一个中间件
  console.log('1. 认证中间件 - 结束');
  return response;
};

// middlewares/logger.ts
export const loggerMiddleware: Route.MiddlewareFunction = async (ctx, next) => {
  console.log('2. 日志中间件 - 开始');
  const response = await next();
  console.log('2. 日志中间件 - 结束');
  return response;
};

// middlewares/timer.ts
export const timerMiddleware: Route.MiddlewareFunction = async (ctx, next) => {
  console.time('request');
  const response = await next();
  console.timeEnd('request');
  return response;
};

// 路由文件中组合
export const middleware = [authMiddleware, loggerMiddleware, timerMiddleware];

假设访问一个路由,并且这个路由有一个 loader 返回数据,执行结果如下:

执行顺序:

  1. 认证中间件 - 开始
  1. 日志中间件 - 开始
    request: 开始计时
    (这里执行路由的 loader)
    request: 123.45ms - 计时结束
  2. 日志中间件 - 结束
  3. 认证中间件 - 结束
  1. 洋葱模型:中间件的执行是嵌套的,像剥洋葱一样
    • 外层中间件先进入,后退出
    • 内层中间件后进入,先退出
  2. await next() 的作用:
    • 调用 await next() 时,当前中间件暂停执行
    • 执行下一个中间件或路由 loader
    • 所有后续逻辑完成后,继续执行当前中间件剩余代码
  3. Response 的传递:

text

loader 返回 Response

timerMiddleware 收到

↓ (return response)

loggerMiddleware 收到

↓ (return response)

authMiddleware 收到

↓ (return response)

客户端收到

注意事项

虽然中间件功能强大,但在使用时需要注意以下几点,以避免性能陷阱。

  • 执行环境:React Router 中间件分为服务器端和客户端两种。
    • 服务器端中间件:在框架模式(Framework Mode)下使用。需要注意的是,对于客户端发起的导航(如点击 ),如果目标路由没有 loader,则默认不会触发服务器端中间件。如果你需要在没有 loader 的路由上也强制运行服务器端中间件,可以手动添加一个空的 loader。
    • 客户端中间件:无论在何种模式下,客户端导航都会触发客户端中间件。
  • 懒加载(Lazy Loading)性能优化:在 v7.5+ 版本中,为了支持中间件的同时不损害性能,React Router 引入了更精细的对象式懒加载API(route.lazy)。
    • 旧问题:旧的函数式 lazy 需要等所有匹配的路由模块加载完,才能开始执行中间件,这会拖慢导航速度。
    • 解决方案:新 API 允许只为 middleware 属性进行懒加载,意味着框架可以立刻执行已加载的中间件,同时后台并行加载其他模块(如 loader 和 Component),从而大幅提升性能。
ts 复制代码
// 推荐:使用新的对象式 API 进行懒加载
const route = {
  path: "dashboard",
  lazy: {
    // 可以独立懒加载中间件,不会被 loader/Component 阻塞
    unstable_middleware: () => import("./dashboard/middleware"),
    // loader 独立懒加载
    loader: () => import("./dashboard/loader"),
    // 组件独立懒加载
    Component: () => import("./dashboard/component"), 
  },
};
  • 首屏关键路由不要懒加载,避免加载延迟影响首次渲染
  • 使用骨架屏作为加载状态,比纯文字提示体验更好
  • 合理拆分粒度:将功能相近的路由合并为一个 chunk,避免请求数量过多
  • 结合预加载:预判用户可能点击的路由,在空闲时提前加载

总结

要点 语法规则
必须调用next() await next() 才能继续执行链
服务器端必须返回Response return response;
客户端不能返回Response 不返回或返回void
短路用throw throw redirect('/login')
多中间件按顺序执行 数组顺序 = 洋葱外→内
context传递数据 set() / get()

十一、useMatches 与 handle(面包屑导航)

useMatches 是 React Router v6+ 中的一个 Hook,用于获取当前页面上所有已匹配的路由信息。它的主要用途是让父级布局组件能够访问到子路由的数据,非常适合用来实现动态面包屑导航或页面标题等需要根据当前路由层级来渲染的 UI 元素。

  1. 使用方法
    首先,你需要从 react-router-dom 中导入它,并在组件中调用。
js 复制代码
import { useMatches } from "react-router-dom";

function Component() {
  const matches = useMatches();
  // matches 是一个数组,包含了当前URL匹配的所有路由
  console.log(matches);
}
  1. 返回值 (Match 对象结构)
    useMatches 返回一个数组,数组中的每个对象都代表一个匹配到的路由层级(从根路由到当前路由)。每个对象的结构如下:
属性 类型 描述
id string 路由的唯一标识符。
pathname string 该路由匹配到的 URL 路径部分。
params object 从 URL 中解析出的参数对象(如 :id 对应的值)。
data unknown 该路由的 loader 函数返回的数据。
handle unknown 你在定义路由时,通过 handle 属性传入的任何应用特定数据。

面包屑导航

第一步:在路由定义中添加 handle

js 复制代码
import { createBrowserRouter } from 'react-router-dom'
import { FatherRoute } from '../components/16.mulRoute/fatherRoute';
import { Contact } from '../components/16.mulRoute/contact';
import { About } from '../components/16.mulRoute/about';
import { Link } from 'react-router-dom';

export const routes = createBrowserRouter([
  {
    path: 'fatherRoute',
    element: <FatherRoute></FatherRoute>,
    handle: {
      crumb: () => <Link to="/fatherRoute/contact">父组件</Link>,  // 定义面包屑文本
    },
    children: [
      {
        path: 'about',
        element: <About />,
        handle: {
          crumb: () => '关于'
        },
      },
      {
        path: 'contact',
        element: <Contact />,
        loader: () => { return { name: '联系' } },
        handle: {
          // crumb 函数可以接收 loader 返回的 data
          crumb: (data: { name: string }) => data.name
        }
      }
    ]
  }
]);

第二步:在main.tsx中导入路由

js 复制代码
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
// import App from './App'
import { RouterProvider } from 'react-router-dom'
import { routes } from './routes'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <RouterProvider router={routes} />
    {/* <App /> */}
  </StrictMode>
)

第三步:声明面包屑导航栏组件

使用 useMatches 获取所有匹配信息,过滤出含有 crumb 的匹配项,然后渲染它们。

ts 复制代码
import type { JSX } from "react";
import { useMatches } from "react-router-dom";

interface RouteHandle {
  crumb: (data: unknown) => React.ReactNode;
}

interface RouteMatch {
  id: string; // 路由的唯一标识符
  pathname: string; // 当前匹配的路径
  params: object; // 路由参数
  handle?: RouteHandle; // 可选的 handle 对象,包含 crumb 函数
  data?: unknown; // 可选的 data 属性,存储 loader 返回的数据
}


export const Breadcrumbs = (): JSX.Element => {
  const matches = useMatches() as RouteMatch[];
  const crumbs = matches
    .filter((match): match is RouteMatch & { handle: RouteHandle } => Boolean(match.handle && match.handle.crumb))
    .map((match) => match.handle.crumb(match.data));
  return (
    <nav>
      {crumbs.map((crumb, index) => (
        <div key={index}>{crumb}</div>
      ))}
    </nav>
  );
}

第四步:使用面包屑导航组件

ts 复制代码
import { Outlet } from 'react-router-dom';
import { Breadcrumbs } from '../17.useMactchRoute/Breadcrumbs';

export const FatherRoute = () => {
  return (
    <div>
      <header>顶部导航(始终显示)</header>
      <Breadcrumbs /> {/* 面包屑导航组件,显示当前路由层级 */}
      <Outlet /> {/* 渲染子路由组件的占位符 */}
      <footer>底部导航(始终显示)</footer>
    </div>
  );
}

注意

  • 需要 Data Router:useMatches 只能与 createBrowserRouter 这类数据路由器配合使用。传统的 无法使用此 Hook。
  • 无法感知后代路由:它只能获取当前已经匹配的路由信息,不会获取当前未激活的后代路由树。

总结:核心 API 速查表

类别 API 用途
路由器 createBrowserRouter + RouterProvider v7 推荐的数据路由器
BrowserRouter 传统声明式路由器
路由匹配 <Routes> + <Route> 声明式路由配置
useRoutes 编程式路由配置
导航 <Link> 声明式跳转
<NavLink> 带激活状态的导航
useNavigate 命令式跳转
参数与状态 useParams 获取动态路由参数
useSearchParams 读写查询字符串
useLocation 获取完整 URL 信息
数据获取 loader + useLoaderData 路由级数据获取
useRouteLoaderData 获取任意路由的 loader 数据
错误处理 errorElement + useRouteError 路由级错误边界
嵌套路由 <Outlet /> 子路由渲染占位符
重定向 redirect loader/action 中重定向
元数据 handle + useMatches 面包屑、页面标题等
中间件 middleware 配置 请求拦截与预处理
相关推荐
IT_陈寒1 小时前
JavaScript的闭包差点让我加班到凌晨
前端·人工智能·后端
JianZhen✓1 小时前
前端面试攻略
前端
CQU_JIAKE1 小时前
[q]4.25
java·开发语言·前端
涵涵(互关)2 小时前
语法大全-only-writer
开发语言·前端·vue.js·typescript
恋猫de小郭2 小时前
Flutter 3.41.8 又双叒修复调试问题,草台班子日常 hotfix
android·前端·flutter
接着奏乐接着舞2 小时前
Cesium 自定义纹理
前端
鹏程十八少2 小时前
9. 2026金三银四 面试官问不垮:Java VS Android 设计模式 16 讲
前端·后端·面试
Csvn2 小时前
前端监控体系
前端
张风捷特烈2 小时前
状态管理大乱斗#04 | Riverpod 源码评析 (上) - 核心架构
android·前端·flutter