react-router-dom的基本使用

npmjs 仓库位置

Setup

1
2
3
4
5
6
npm create vite@latest name-of-your-project -- --template react
# follow prompts
cd <your new project directory>
npm install react-router-dom # always need this!
npm install localforage match-sorter sort-by # only for this tutorial.
npm run dev

有:

1
2
3
4
VITE v3.0.7  ready in 175 ms

➜ Local: http://127.0.0.1:5173/
➜ Network: use --host to expose

(提示了 VITE 开发的端口信息)

入口文件

./src/main.jsx 是入口文件.

添加一个路由

需要先创建一个 Browser Router (React Router 提供的一种组件类型).

之后在 Browser Router 中配置程序的第一个路由 (相当于默认打开时访问的位置). 简单来说就两步:

  • 创建
  • 提供
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
./main.jsx
*/
import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import "./index.css";

const router = createBrowserRouter([
{
path: "/",
element: <div>Hello world!</div>,
},
]);

ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);

createBrowserRouter() 接受一个对象数组作为参数, 对象的 path 成员对应路由路径, element 对应渲染的页面元素 (替换为自己的根元素).

RouterProvider 组件, 用于将整个路由配置 (也就是一个 Browser Router) 传递给 React 应用程序.

错误处理

通过设置 createBrowserRouter() 创建对象的 errorElement 成员. (先用 useRouteError() 获取错误信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/error-page.jsx
import { useRouteError } from "react-router-dom";

export default function ErrorPage() {
const error = useRouteError();
console.error(error);

return (
<div id="error-page">
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
}


// src/main.jsx
/* previous imports */
import ErrorPage from "./error-page";

const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
},
]);

ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);

The Contact Route UI

1
2
3
4
5
6
7
8
9
10
11
12
13
import Contact from "./routes/contact";

const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
},
{
path: "contacts/:contactId",
element: <Contact />,
},
]);

这里的 :contactId 是一个占位符 (: 称 dynamic segment, 是必需的, 而 contactId 是自己命名的), 若访问路由 contacts/xxxxx, 则 xxxxx 会被捕获并传递给 <Contact /> 组件.

比如访问 /contacts/123, 那么在 <Contact> 组件中, 调用 useParams(), 会返回 { contactId: '123' }.

嵌套路由

1
2
3
4
5
6
7
8
9
10
11
12
13
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);

此时, 需要在 <Root> 组件中, 通过 <Outlet> 组件 (react-router-dom 提供) 来指定 children 的 element 需要渲染在哪个地方:

1
2
3
4
5
6
7
8
9
10
11
12
import { Outlet } from "react-router-dom";

export default function Root() {
return (
<>
{/* all the other elements */}
<div id="detail">
<Outlet />
</div>
</>
);
}

Client Side Routing

Client 指打开页面的用户, 另一侧是 Server, 提供页面.

Client Side Routing 指由用户端浏览器已经加载的 JavaScript 来管理路由, 而不是向服务器请求一个新页面. 这样可以提供更流畅的用户体验, 因为应用程序可以在不重新加载整个页面的情况下更新页面内容.

在这里通过 <Link> 组件替换掉原来的 <a href> 来实现. 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Outlet, Link } from "react-router-dom";

export default function Root() {
return (
<>
<div id="sidebar">
{/* other elements */}

<nav>
<ul>
<li>
<Link to={`contacts/1`}>Your Name</Link>
</li>
<li>
<Link to={`contacts/2`}>Your Friend</Link>
</li>
</ul>
</nav>

{/* other elements */}
</div>
</>
);
}

路径与组件的关联

URL 段一般设计和相关组件有关, 比如:

  • /<Root> 相关
  • contacts/:id<Contact> 相关

数据加载

可以利用 loader 在渲染组件之前预加载数据. 它在路由定义时指定, 并且在特定路由被访问时调用. 所加载的数据通常与该路由相关, 并且可以通过 useLoaderData 钩子在组件中访问.

首先, 定义一个 loader 函数. 这个函数可以是异步的, 用于获取所需的数据, 例如从 API 请求数据, 也可以在使用 loader 的组件中通过 useLoaderData 钩子获取 loader 返回的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// routes/root.js
export async function loader() {
// 这里可以是从 API 获取数据的异步操作
const data = await fetch('/api/data').then(response => response.json());
return data;
}

export default function Root() {
const data = useLoaderData();

return (
<div>
<h1>Root Component</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}

之后配置路由并指定 loader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// router.js
import { createBrowserRouter } from "react-router-dom";
import Root, { loader as rootLoader } from "./routes/root";
import ErrorPage from "./components/ErrorPage";
import Contact from "./components/Contact";

const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);

export default router;

利用 HTML 表单进行导航

链接导航

传统的是链接导航, 即使用 <a> 标签创建超链接, 用户可以点击链接进行页面跳转, 如:

1
<a href="/about">About Us</a>

在 React 中为:

1
2
3
import { Link } from "react-router-dom";

<Link to="/about">About Us</Link>

表单导航

而 HTML 表单导航为: 通过 HTML 表单元素 (<form>) 及其相关属性和方法来实现页面之间的导航和数据提交.

一个典型的 HTML 表单包括以下几个基本部分:

  • <form> 标签: 定义表单的开始和结束
  • 输入控件: 如文本框, 复选框, 单选按钮等, 用于用户输入数据
  • 提交按钮: 用于提交表单数据

如:

1
2
3
4
5
<form action="/submit-form" method="post">
<label for="name">Name:</label>
<input type="text" id="name" name="name">
<button type="submit">Submit</button>
</form>

这里有两个表单属性:

  • action: 指定表单数据提交的目标 URL
  • method: 指定表单数据提交的方法, 通常是 GET 或 POST

其工作原理为:

  1. 用户输入数据: 用户在表单中输入数据
  2. 提交表单: 用户点击提交按钮 (<button type="submit">) 或触发提交事件
  3. 数据序列化: 浏览器将表单数据序列化为键值对
  4. 发送请求: 浏览器根据 action 和 method 属性, 将表单数据发送到指定服务器
  5. 处理响应: 服务器处理请求并返回响应, 浏览器根据响应执行相应的操作, 如页面重定向或显示结果

其与传统的链接导航主要区别在于可以指定请求方式如 GET, POST.

React 中为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Form } from "react-router-dom";

function ContactForm() {
return (
<Form method="post" action="/create-contact">
<label>
Name:
<input type="text" name="name" />
</label>
<button type="submit">Create Contact</button>
</Form>
);
}

export default ContactForm;

在 React Router 中, 该表单会提交给客户端路由 (这里的 action 所指定的路由), 而不是服务器.

而路由通过设置 action 成员来确定用哪个函数来处理, 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// form.jsx
import { Form } from "react-router-dom";

function MyForm() {
return (
<Form method="post">
<button type="submit">New</button>
</Form>
);
}

export default MyForm;

// main.jsx
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import MyForm from "./MyForm";
import HomePage from "./HomePage";
import ErrorPage from "./ErrorPage";

async function handleFormSubmit({ request }) {
const formData = await request.formData();
// 处理表单数据,例如将其发送到服务器
console.log("Form Data:", Object.fromEntries(formData));
// 返回一个响应,可能包括重定向或其他操作
return null;
}

const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
errorElement: <ErrorPage />,
children: [
{
path: "current-page",
element: <MyForm />,
action: handleFormSubmit,
},
],
},
]);

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

export default App;

在 Loader 中使用 URL Params

对于:

1
2
3
4
5
6
[
{
path: "contacts/:contactId",
element: <Contact />,
},
];

这里 contactId 捕获的数据会以第一个变量传递给 loader 指定的函数, 如:

1
2
3
4
export async function loader({ params }) {
const contact = await getContact(params.contactId);
return { contact };
}

以此来访问.

利用表单更新数据

如:

1
2
3
4
5
6
7
<input
placeholder="First"
aria-label="First name"
type="text"
name="first"
defaultValue={contact.first}
/>

若想访问表单提交过来的数据:

1
2
3
4
5
6
export async function action({ request, params }) {
const formData = await request.formData();
const firstName = formData.get("first");
const lastName = formData.get("last");
// ...
}

先用 fromData() 得到数据主体, 之后用 get() 获取指定字段.

若想用常用的字段访问方式, 则用 fromEntries() 获取数据体:

1
2
3
const updates = Object.fromEntries(formData);
updates.first; // "Some"
updates.last; // "Name"

示例:

1
2
3
4
5
6
7
8
9
10
11
import {
redirect,
} from "react-router-dom";
import { updateContact } from "../contacts";

export async function action({ request, params }) {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await updateContact(params.contactId, updates);
return redirect(`/contacts/${params.contactId}`);
}

注意这里的 redirect(), 返回一个 redirect response, 及让 client router 进行跳转处理.

对于 action 处指定的函数而言, 默认传入两个参数:

  • 表单产生的 request
  • URL params

NavLink

NavLink 是 React Router 中的一个组件, 用于创建导航链接, 并且可以根据当前 URL 的匹配情况为链接添加样式. 与普通的 Link 组件不同, NavLink 组件提供了 active 状态的管理.

1
import NavLink from "react-router-dom";

主要属性有:

  • to: 指定导航链接的目标路径
  • exact: 仅在路径完全匹配时才应用活动状态
  • activeClassName: 指定在匹配时应用的 CSS 类名,默认为 active
  • activeStyle: 指定在匹配时应用的内联样式
  • isActive: 一个函数,返回布尔值,用于自定义活动状态的逻辑

示例:

1
2
3
4
5
6
7
8
9
10
11
12
<NavLink
to={`contacts/${contact.id}`}
className={({ isActive, isPending }) =>
isActive
? "active"
: isPending
? "pending"
: ""
}
>
{/* other code */}
</NavLink>

当页面处于 NavLink 中的 URL 时, 即为 active.

导航状态处理

假设我们有一个博客应用, 用户点击文章标题时会导航到文章详情页. 在导航过程中,我们希望显示一个加载指示器. 我们可以使用 useNavigation 来实现这个功能.

useNavigation 钩子返回的导航状态对象包含了几个与当前导航状态相关的属性:

  1. state, 表示当前的导航状态:
    • idle: 当前没有进行中的导航
    • loading: 正在进行导航, 通常是因为一个异步操作 (如数据获取) 正在进行
    • submitting: 表单正在提交
  2. location: 当前的 Location 对象, 包含了关于当前 URL 的信息, 如 pathname, search, hash 等
  3. navigationType: 表示当前导航的类型, 可以是以下值之一:
    • PUSH: 用户通过点击链接或调用 navigate 方法进行的导航。
    • POP: 用户通过浏览器的前进或后退按钮进行的导航。
    • REPLACE: 用户通过调用 navigate 方法的 replace 选项进行的导航

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {
// existing code
useNavigation,
} from "react-router-dom";

// existing code

export default function Root() {
const { contacts } = useLoaderData();
const navigation = useNavigation();

return (
<>
<div id="sidebar">{/* existing code */}</div>
<div
id="detail"
className={
navigation.state === "loading" ? "loading" : ""
}
>
<Outlet />
</div>
</>
);
}

这里通过判断 state 来确定要使用的样式.

异常处理

使用 errorElement, 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// destroy.jsx
export async function action({ params }) {
throw new Error("oh dang!");
await deleteContact(params.contactId);
return redirect("/");
}

// main.jsx
...
[
/* other routes */
{
path: "contacts/:contactId/destroy",
action: destroyAction,
errorElement: <div>Oops! There was an error.</div>,
},
];
...

添加 Index Routes

当访问 / 时, 此时 children 处的 path 无法 match, 因此不会有任何显示, 可以通过添加一个 index route 来解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/routes/index.jsx
export default function Index() {
return (
<p id="zero-state">
This is a demo for React Router.
<br />
Check out{" "}
<a href="https://reactrouter.com">
the docs at reactrouter.com
</a>
.
</p>
);
}

// src/main.jsx
// existing code
import Index from "./routes/index";

const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{ index: true, element: <Index /> },
/* existing routes */
],
},
]);

页面回退

利用 useNavigation() 可以实现回退到 browser’s history 的上一个 entry:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import {
Form,
useLoaderData,
redirect,
useNavigate,
} from "react-router-dom";

export default function EditContact() {
const { contact } = useLoaderData();
const navigate = useNavigate();

return (
<Form method="post" id="contact-form">
{/* existing code */}

<p>
<button type="submit">Save</button>
<button
type="button"
onClick={() => {
navigate(-1);
}}
>
Cancel
</button>
</p>
</Form>
);
}

处理 URL Search Params 以及 GET Submissions

如:

1
2
3
4
5
6
7
8
9
10
11
<Form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div id="search-spinner" aria-hidden hidden={true} />
<div className="sr-only" aria-live="polite"></div>
</Form>

可以发送 http://127.0.0.1:5173/?q=xxx 的请求.

处理如:

1
2
3
4
5
6
export async function loader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts };
}

注意这里不是 POST 请求, 而是 GET 请求, 因此 React Router 不会触发 action, 代码也是添加到 loader 中.


react-router-dom的基本使用
http://example.com/2024/07/23/react-router-dom的基本使用/
作者
Jie
发布于
2024年7月23日
许可协议