这个只是专门讲解 React Router 新开的例子。
教程来源:https://reactrouter.com/en/main/start/tutorial
yarn create vite my-react-router-app --template react-ts
cd my-react-router-app
yarn
安装 React Router 依赖:
yarn add react-router-dom localforage match-sorter sort-by
# 对于 ts 要添加 @types/sort-by, 否则报错找不到模块【不影响运行】
yarn add -D @types/sort-by
运行项目
yarn dev
# VITE v4.4.11 ready in 358 ms
# ➜ Local: http://localhost:5173/
# ➜ Network: use --host to expose
# ➜ press h to show help
获取该项目所需要的文件
这里找到 css 文件,将其粘贴到 src/index.css
文件中: Copy/Paste the tutorial CSS
这里找到 js 文件,将其添加到 src/contacts.js
:Copy/Paste the tutorial CSS
Js 文件主要的用途:将创建、阅读、搜索、更新和删除数据。一个典型的Web应用程序可能会与Web服务器上的API对话,将使用浏览器存储并伪造一些网络延迟来保持这一点。这些代码都与React Router无关,所以直接复制/粘贴即可。
2个文件拷贝到项目中后,你可以将无关文件删除,可以删除任何其他内容(如 App.js
和 assets
等)。
项目主要文件(文件目录):
src
├── contacts.js # 这里最好转换成 ts 文件,即添加上类型,方便后面
├── index.css
└── main.tsx
在 main.tsx
中创建并渲染浏览器路由器
// 导入路由模块
import {
createBrowserRouter,
RouterProvider
} from "react-router-dom"
// 配置路由
const router = createBrowserRouter([
{
// 根路由 root router
path: "/",
// 之后会替换成组件
element: Hello world!
}
])
// 渲染:将其添加到 render 函数中作为参数传递即可
ReactDOM.createRoot(document.getElementById('root')!).render(
,
)
创建 src/routes
和 src/routes/root.tsx
rout.tsx 文件如下:
export default function Root() {
return (
<>
>
);
}
回到 src/main.tsx
的 router 变量处,将
替换成组件。
import Root from './routes/root'
// 配置路由
const router = createBrowserRouter([
{
path: "/",
element:
}
])
好了,基本结构搭建完毕!运行项目看看吧。
yarn dev
以前在写 React 的时候,因为是单页面应用,所以地址栏的变化不会影响程序的影响。
但是导入了 React Router 组件后,地址栏会被它所监听到,从而出现 React Router 默认的错误屏幕。
创建 src/error-page.tsx
文件。
import { useRouteError } from "react-router-dom";
export default function ErrorPage() {
const error = useRouteError();
console.error(error);
return (
Oops!
Sorry, an unexpected error has occurred.
{error.statusText || error.message}
);
}
回到 src/main.tsx
文件。
将根路由上的
设置为 errorElement
import ErrorPage from './error-page'
// 配置路由
const router = createBrowserRouter([
{
path: "/",
element: ,
errorElement:
}
])
然后我们随机访问一个页面,http://localhost:5173/contacts/1 会显示我们创建的错误页面(ErrorPage 组件)。
useRouteError
提供了抛出的错误。当用户导航到不存在的路由时,您将得到一个错误响应,并显示“Not Found” statusText
。
有了根路由,那么其他页面呢?
创建一个 src/routes/contact.tsx
文件
import { Form } from "react-router-dom";
export default function Contact() {
const contact = {
first: "Your",
last: "Name",
avatar: "https://placekitten.com/g/200/200",
twitter: "your_handle",
notes: "Some notes",
favorite: true,
};
return (
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
>
) : (
No Name
)}{" "}
{contact.twitter && (
)}
{contact.notes && {contact.notes}
}
);
}
function Favorite({ contact }) {
// yes, this is a `let` for later
let favorite = contact.favorite;
return (
);
}
然后我们到 src/main.tsx
进行添加路由信息,还是配置那里。
// 配置路由
const router = createBrowserRouter([
//....
{
path: "contacts/:contactId",
element: ,
errorElement:
}
])
会发现,它不在我们的根布局中(右侧没有 组件,即侧边导航栏没有看到)
如果希望contact组件在
布局中呈现。
我们通过使联系路由成为根路由的子路由
来实现这一点。
回到我们的 src/main,tsx
的配置路由信息:
// 将添加的 contacts 路由信息移动到 Root 的 children 属性上
// 配置路由
const router = createBrowserRouter([
{
//....
children: [
{
path: "contacts/:contactId",
element:
}
]
},
])
再次看到根布局,但右侧是一个空白页面。我们需要告诉根路由我们希望它在哪里渲染其子路由。
回到我们的 组件中(src/routes/rout.tsx
)
Render an Outlet 渲染一个
// 导入
import { Outlet } from "react-router-dom"
// 在哪里进行渲染
Client Side Routing 。
在路由跳转之间,我们发现,浏览器是请求整个网页文档。而不是使用 React Router。
客户端路由允许我们的应用更新URL,而无需从服务器请求另一个文档。【立即呈现新的UI】
使用 组件
import { Link } from "react-router-dom"
// 将 a 标签替换成
前面我们只是将数据写死,该节内容就是如何通过 React Router 调用 API 去获取数据。
注意:这是一个 Demo,真实的情况请采用 axios 去获取后端接口。
到我们的根路由组件()
import { getContacts } from "../contacts"
export async function loader() {
const contacts = await getContacts();
return { contacts };
}
再来到 src/main.tsx
文件,进行配置 loader
import Root, { loader as rootLoader } from './routes/root'
// 配置路由
const router = createBrowserRouter([
{
// ...
loader: rootLoader,
// ...
},
])
然后回到我们的根路由组件()
import { Outlet, Link, useLoaderData } from "react-router-dom"
// 其他代码...
export default function Root() {
const { contacts }: any = useLoaderData();
return (
<>
{* 其他代码... *}
{* 其他代码... *}
>
)
}
好了,看看页面吧~
在页面中我们可以看到没有联系人列表。
这个时候我们需要完成新建功能,看到 New 按钮了吗?
点击一下会发生什么?呜?出错了?找不到 localhost 的网页
这里正常情况会报 405 错误,找不到对应的请求方式。
这里可以采取两种方式: 给 vite 增加 post 提交功能,或者采用 CSR 客户端路由方式【本节重点】。
回到我们的根路由组件()
import { Outlet, Link, useLoaderData, Form } from "react-router-dom"
import { getContacts, createContact } from "../contacts";
// 新增联系人
export async function action() {
const contact = await createContact();
return { contact };
}
// ...
return (
// ...
)
回到 src/main.tsx
配置 action
import Root, { loader as rootLoader, action as rootAction } from './routes/root'
// 配置路由
const router = createBrowserRouter([
{
path: "/",
element: ,
errorElement: ,
loader: rootLoader,
action: rootAction,
//...
])
但是思考一下。
当我们点击 New 后,页面如何更新的? action
在哪里?重新获取数据的代码在哪里? useState
、 onSubmit
、 useEffect
在哪里?!
阻止浏览器向服务器发送请求,而是将其发送到路由
action
。在Web语义中,POST通常意味着某些数据正在更改。按照惯例,React Router将此作为提示,在操作完成后自动重新验证页面上的数据。这意味着你所有的 useLoaderData
钩子都会更新,UI会自动与你的数据保持同步!
所以,原理在于 React Router 根据 POST (意味着要更新数据了),去自动帮助我们更新源代码。
当点击联系人列表的某个名称时,会跳转到某个页面,只是 id 变成我们模拟出来的了,而不是写死的。
但是这里有个问题,右侧还是 组件,数据没有进行更新。
在 也需要 loader ,这里与根路由一样,不再阐述,只会给关键代码。
// src/routes/contact.tsx
import { Form, useLoaderData } from "react-router-dom"
import { getContact } from "../contacts"
export async function loader({ params }: any) {
const contact = await getContact(params.contactId)
return { contact }
}
export default function Contact() {
const { contact }: any = useLoaderData();
// ...
}
// src/main.tsx
import Contact, { loader as contactLoader } from './routes/contact'
// 配置路由
const router = createBrowserRouter([
{
//....
children: [
{
path: "contacts/:contactId",
element: ,
loader: contactLoader
}
]
},
])
一个新文件 src/routes/edit.tsx
import { Form, useLoaderData } from "react-router-dom";
// 自己补充 loader ,参考 contact.tsx
export default function EditContact() {
const { contact } = useLoaderData();
return (
);
}
// src/main.tsx
import EditContact, { loader as editLoader } from './routes/edit'
// 配置路由
const router = createBrowserRouter([
{
//...
{
path: "contacts/:contactId/edit",
element: ,
loader: editLoader
}
]
},
])
除了添加 loader 外,还应该添加 action
// src/routes/edit.tsx
import { Form, useLoaderData, redirect } from "react-router-dom";
import { getContact, updateContact } from "../contacts"
export async function action({ request, params }: any) {
const formData = await request.formData()
const updates = Object.fromEntries(formData)
await updateContact(params.contactId, updates)
return redirect(`/contacts/${params.contactId}`)
}
// src/main.tsx
// 配置路由
const router = createBrowserRouter([
{
//...
children: [
{
//..
},
{
path: "contacts/:contactId/edit",
element: ,
loader: editLoader,
action: editAction
}
]
},
])
Loaders and actions 都能够返回 Response (它们都能接收 request 参数)。
redirect 重定向到一个地址。
如果没有客户端路由,如果服务器在POST请求后重定向,则新页面将获取最新数据并呈现。正如我们之前了解到的,React Router模拟了这个模型,并在操作之后自动重新验证页面上的数据。这就是为什么侧边栏会在我们保存表单时自动更新。如果没有客户端路由,额外的重新验证代码就不存在,所以它也不需要在客户端路由中存在!
小插曲 ,更新 “New” 按钮的 action ,
在 组件中,经过前面这么多步骤,自己也能够实现
随着左侧的导航栏联系人越来越多,我们分不清右侧人物是左侧哪个标签。
可以采用
// src/routes/root.tsx
import {
Outlet,
NavLink,
useLoaderData,
Form,
redirect,
} from "react-router-dom";
return (
// ...
isActive
? "active"
: isPending
? "pending"
: ""
}
>
{/* other code */}
)
使用 Global Pending UI
使用到的 API : useNavigation
。通过它,我们可以知道该页面是否处于加载中。
在短延迟后添加一个漂亮的淡入淡出(以避免快速加载的UI闪烁)。你可以做任何你想做的事情,比如显示一个加载条在顶部。
在跳转过程时,会发现第二次明显加快不少,这是有缓存。
// src/routes/root.tsx
import {
// existing code
useNavigation,
} from "react-router-dom";
export default function Root() {
const { contacts } = useLoaderData();
const navigation = useNavigation();
return (
<>
>
);
}
这里与 edit.tsx 类似,同样创建文件,添加 action
// src/routes/destroy.tsx 【自己创建】
import { redirect } from "react-router-dom";
import { deleteContact } from "../contacts";
export async function action({ params }: any) {
await deleteContact(params.contactId);
return redirect("/");
}
// src/main.tsx
/* existing code */
import { action as destroyAction } from "./routes/destroy";
const router = createBrowserRouter([
{
path: "/",
/* existing root route props */
children: [
/* existing routes */
{
path: "contacts/:contactId/destroy",
action: destroyAction,
},
],
},
]);
/* existing code */
Index Routes
在浏览首页时,右侧总是空白,我们可以设置一个组件显示在那里。
// src/routes/index.tsx
export default function Index() {
return (
This is a demo for React Router.
Check out{" "}
the docs at reactrouter.com
.
);
}
// src/main.tsx
// existing code
import Index from "./routes/index";
const router = createBrowserRouter([
{
path: "/",
element: ,
errorElement: ,
loader: rootLoader,
action: rootAction,
children: [
{ index: true, element: },
/* existing routes */
],
},
]);
我们需要在按钮上添加一个click处理程序,以及React Router中的 useNavigate
。
// src/routes/edit.tsx
import {
Form,
useLoaderData,
redirect,
useNavigate,
} from "react-router-dom";
export default function EditContact() {
const { contact } = useLoaderData();
const navigate = useNavigate();
return (
);
}
在前面我们,我们分别针对增删改,都做了处理。
不仅仅只有上面,还保存到 indexDb 中哦,这个演示已经做了很多了,
现在来做搜索。
现在将 form 改成 React Router 做 CSR 【客户端路由】
// src/routes/root.jsx
<Form id="search-form" role="search">Form>
因为这是 GET 请求,它不会经过 action,将过滤代码写在 loader 中。
// src/routes/root.jsx
// 获取联系人列表
export async function loader({ request }: any) {
// 过滤
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts };
}
现在回到页面,进行简单的输入一些字符。就能看到查询的结构了。
前面只是简单的进行搜索。
问题:
第一个问题比较好解决,给搜索框一个默认值。
// src/routes/root.jsx
export async function loader({ request }: any) {
//...
return { contacts, q };
}
const { contacts, q }: any = useLoaderData();
解决第二个问题,搜索框会残留之前的搜素记录。
当数据发生变化时,对搜素框的值进行修改,这里可以用到 useEffect(callback, [q])
// src/routes/root.jsx
const [query, setQuery] = useState(q);
// 当查询 q 变化时,更新 query
useEffect(() => {
setQuery(q);
}, [q])
// 将查询值作为搜素框的值
// 在搜索框输入时,记得监听,更新 query 值
{
setQuery(e.target.value)
}}
前面的搜素,只有我们按下回车键才会进行表单提交,才会过滤后的结果。
使用 React Router 的 API:useSubmit
// src/routes/root.jsx
import { useSubmit } from "react-router-dom"
// 获取 submit
const submit = useSubmit();
// 提交表单组件
onChange={e => {
// currentTarget 与 target
submit(e.currentTarget.form);
}}
这里有个小小知识点:
因为搜素是在网络请求,可能网络不太好或者请求数据量大。使用 useNavigation 获取加载状态。
// src/routes/root.jsx
// 是否在搜素
const searching = navigation.location && new URLSearchParams(navigation.location.search).has("q");
// 修改搜索框样式
// 修改搜索框图标
优化自动提交,useSubmit()
submit 有其他配置,如 replace。
设置为true,替换浏览器历史记录堆栈中的当前条目,而不是创建一个新条目(即保持在“同一页面”)。默认为false。
submit(e.currentTarget.value, {
replace: true
})
我们只想替换搜索结果,而不是开始搜索之前的页面。
const isFirstSearch = q == null;
submit(e.currentTarget.value, {
replace: !isFirstSearch;
})
在前面都是通过修改地址栏,通过导航的方式切换页面,使页面上的数据发生改变。
在该节内容中,我们将学习到如何通过 useFetcher API 去更改数据。
我们有 useFetcher
钩子。它允许我们与 loader 和 action 进行通信,而不会导致导航。
比如该项目中联系人页面上的★按钮对此很有意义。我们不是在创建或删除新记录,我们不想更改页面,我们只是想更改我们正在查看的页面上的数据。
// src/routes/contact.tsx
import { useFetcher } from "react-router-dom"
// 使用
const fetcher = useFetcher();
return (
)
对于 post 提交我们还要 action 来处理。
export async function action({ request, params }: any) {
const formData = await request.formData();
return updateContact(params.contactId, {
favorite: formData.get("favorite") === "true"
})
}
将 action 加入到 src/main.tsx 中,这一步跳过,较简单。
在点击收藏按钮时,明显感到迟钝。
在 Favorite 加上下面代码:
if (fetcher.formData) {
favorite = fetcher.formData.get("favorite") === "true"
}
加上后,会立即更新状态。
当访问一个未知的联系人时,报错:
Cannot read properties of null (reading 'avatar')
在获取联系人(loader)中去判断。
// src/routes/contact.tsx
export async function loader({ params }) {
const contact = await getContact(params.contactId);
if (!contact) {
throw new Response("", {
status: 404,
statusText: "Not Found",
});
}
return { contact };
}
将报错信息,都放在根路由上。(即:左侧有侧边导航栏)
// 配置路由
const router = createBrowserRouter([
{
// 根路由
path: "/",
element: ,
errorElement: ,
loader: rootLoader,
action: rootAction,
children: [
{
// 单独拉出一个路由,去处理错误页面
errorElement: ,
children: [
// 子路由
]
}
]
},
])
可以不使用 createBrowserRouter 配置路由。
可以采取 jsx 的风格去编写路由。
import {
createRoutesFromElements,
createBrowserRouter,
Route,
} from "react-router-dom";
const router = createBrowserRouter(
createRoutesFromElements(
}
loader={rootLoader}
action={rootAction}
errorElement={ }
>
}>
} />
}
loader={contactLoader}
action={contactAction}
/>
}
loader={contactLoader}
action={editAction}
/>
)
);
更多 API 访问 React Router 官方: React-Router v6.16.0