我们书接上回,继续我们的React Router 路由之路:
我们到目前为止都没有用到 state、useEffect、redux等状态管理器。但也达到了我们的设计目的。
注意,action 返回的结果 可以在组件中使用
useActionData()
来获取。就像useLoaderData()
的使用一样。
loader 中的 URL参数
接着上回的示例文件, 新建几个无名联系人。虽然联系人信息没有变化,但我们仔细查看地址栏,会发现 ID
是会发生变化的。因为我们在createContact()
中为每个联系人分配了一个ID号。
我们再次查看路由信息,
javascript
...
{
path: "contacts/:contactId",
element: <Contact />,
},
...
在上节中我们已经详细的说过,通过 :contactId
路由变量,我们可以获取到这个符在地址中的 id
号的。我们再次对 utils.jsx
进行修改:注意里面的备注信息,很重要。
javascript
import { getContacts, createContact, getContact } from "./contacts";
// 模拟网络请求,获取数据
export async function rootLoader() {
const contacts = await getContacts();
return { contacts };
}
//创建新的联系人
export async function rootAction() {
const contact = await createContact();
return { contact };
}
// 根据 URL 中的ID 获取对应的联系人信息。变量 params 是一个对象,包含了 URL 中的参数。
// 其中 contactId 就是路由变量中的 :contactId,即 /contacts/:contactId。同名变量。
export async function contactLoader({ params }) {
const contact = await getContact(params.contactId);
return { contact };
}
为了不造成理解上的概念混淆,我们把原先的action
更名为 rootAction
, 新增了 contactLoader
函数,你应该理解过来了,这个loader是用有 组件的路由配置中的。 然后在 routerConfig.jsx
中进行修改。
javascript
import { createBrowserRouter } from "react-router-dom";
import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import {
rootLoader,
rootAction,
contactLoader,
} from "./utils";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <Error404 />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader
},
]
}
]);
export default router;
现在组件<Contact/>
加载后就能获取到相应的 contact
信息了,我们只要在组件内用 useLoaderData()
就能获取到。对Contact.jsx
进行修改:
javascript
// Contact.jsx
import {
Form,
useLoaderData,
} from "react-router-dom";
import Favorite from "./Favorite";
export default function Contact() {
const { contact } = useLoaderData();
// const contact = {
// first: "Your",
// last: "Name",
// avatar: "https://placekitten.com/g/200/200",
// twitter: "your_handle",
// notes: "Some notes",
// favorite: true,
// };
...
}
将之前表态定义的联系人信息删掉,用 上面的替换。 上面我已经注释了。
更新联系人信息
就像创建数据一样,您可以使用 更新数据。创建 EditContact.jsx 文件。
javascript
// Edit.jsx
import { Form, useLoaderData } from "react-router-dom";
export default function EditContact() {
const { contact } = useLoaderData();
return (
<Form method="post" id="contact-form">
<p>
<span>Name</span>
<input
placeholder="First"
aria-label="First name"
type="text"
name="first"
defaultValue={contact.first}
/>
<input
placeholder="Last"
aria-label="Last name"
type="text"
name="last"
defaultValue={contact.last}
/>
</p>
<label>
<span>Twitter</span>
<input
type="text"
name="twitter"
placeholder="@jack"
defaultValue={contact.twitter}
/>
</label>
<label>
<span>Avatar URL</span>
<input
placeholder="https://reactrouter.com/_docs/tutorial/12.webp"
aria-label="Avatar URL"
type="text"
name="avatar"
defaultValue={contact.avatar}
/>
</label>
<label>
<span>Notes</span>
<textarea
name="notes"
defaultValue={contact.notes}
rows={6}
/>
</label>
<p>
<button type="submit">Save</button>
<button type="button">Cancel</button>
</p>
</Form>
);
}
增加路由信息,将编辑组件添加到路由中
javascript
// routerConfig.jsx
import { createBrowserRouter } from "react-router-dom";
import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import EditContact from "./EditContact";
import {
rootLoader,
rootAction,
contactLoader,
} from "./utils";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <Error404 />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader
},
{
path: "contacts/:contactId/edit",
element: <EditContact />,
loader: contactLoader,
},
]
}
]);
export default router;
配置在子路由中的目的就是在 Root
中的 <Outlet/>
的位置中显示的。
注意路由的配置,当我们点击 Edit 按钮后,注意查看 URL 的变化,如:http://localhost:5173/contacts/uiahrwo/edit
, 一眼就能看出可以匹配 ath: "contacts/:contactId/edit"
的配置信息。
现在在界面应该如下所示:
现在我人需要将编辑的信息反映到 action
中来更新信息。在 utils.jsx
中创建 editAction()
, 并配置到路由中。文件中的参数我已经作了详细的说明。
javascript
// utils.jsx
import { redirect } from "react-router-dom";
import { getContacts, createContact, getContact, updateContact } from "./contacts";
...
// 更新联系人信息
// request 是一个对象,包含了请求的所有信息,包括请求头、请求体等。
// params 是一个对象,包含了 URL 中的参数,即路由变量。
// 通过 request.formData() 可以获取到表单数据,返回一个 FormData 对象。
// FormData 对象是一个键值对集合,每个键对应一个值,值可以是字符串,也可以是 Blob 对象。
// 通过 Object.fromEntries() 可以将 FormData 对象转换为一个普通的对象。
// 通过 updateContact() 更新联系人信息。
// redirect() 可以重定向到指定的 URL。
export async function editAction({ request, params }) {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await updateContact(params.contactId, updates);
return redirect(`/contacts/${params.contactId}`);
}
更新路由配置信息:
javascript
// routerConfig.jsx
import { createBrowserRouter } from "react-router-dom";
import Root from "./Root";
import Error404 from "./Error404";
import Contact from "./Contact";
import EditContact from "./EditContact";
import {
rootLoader,
rootAction,
contactLoader,
editAction,
} from "./utils";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <Error404 />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader
},
{
path: "contacts/:contactId/edit",
element: <EditContact />,
loader: contactLoader,
action: editAction
},
]
}
]);
export default router;
增加活动链接状态
现在我们有一堆记录,不清楚我们在侧边栏中查看的是哪一条。我们可以使用 NavLink
来解决这个问题。
将 Root
中的 Link
替换为 NavLink
,如下所示:
javascript
import {
Outlet,
Link,
useLoaderData,
Form,
NavLink,
} from 'react-router-dom';
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<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>
<Form method="post">
<button type="submit">New</button>
</Form>
</div>
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<NavLink
to={`contacts/${contact.id}`}
className={({ isActive, isPending }) =>
isActive
? "active"
: isPending
? "pending"
: ""
}
>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
{contact.favorite && <span>★</span>}
</NavLink>
</li>
))}
</ul>
) : (
<p>
<i>No contacts</i>
</p>
)}
</nav>
</div>
<div id="detail"> <Outlet /> </div>
</>
);
}
请注意,我们将一个函数传递给 className
。当用户位于 NavLink
中的 URL 时,isActive
将为 true
。当它即将处于活动状态(数据仍在加载)时,isPending
将为 true
。这使我们能够轻松指示用户的位置,并就已点击但仍在等待数据加载的链接提供即时反馈。
有时候我们希望数据在加载的时候有个状态反馈,如下图所示:
路由的加载状态可以通过 useNavigation 来获取。我们再次对 Root.jsx 进修修改:
javascript
import {
// 其它代码
// ... ...
useNavigation,
} from "react-router-dom";
// ...
export default function Root() {
const { contacts } = useLoaderData();
const navigation = useNavigation();
return (
<>
<div id="sidebar">
{/* ... */}
</div>
<div
id="detail"
className={
navigation.state === "loading" ? "loading" : ""
}
>
<Outlet />
</div>
</>
);
}
由于我们在本地缓存了数据,所以加载速度很快,看不到这个效果,但如果是从网络上执行耗时的操作时,这种效果就很明显了。
删除联系人
现在我们再来完善我们的应用,当我们点击删除按钮后,我们要把 联系人的 ID
传递给路由,再由相应的action
去完成删除工作。
查看 Contact.jsx
文件
javascript
<Form
method="post"
action="destroy"
onSubmit={(event) => {
if (
!confirm(
"Please confirm you want to delete this record."
)
) {
event.preventDefault();
}
}}
>
<button type="submit">Delete</button>
</Form>
仔细查看<Form>
中的参数, action
提交到 destroy
路由,这个路由地址是相对地址。由于Contact
的路由是 contacts/:contactId
, 所以这个destroy
的路径为 contacts/:contactId/destroy
,
我们先创建这个路由的action
函数: deleteAction()
,
javascript
// utils.jsx
import { redirect } from "react-router-dom";
import {
getContacts,
createContact,
getContact,
updateContact,
deleteContact
} from "./contacts";
...
// 删除联系人
export async function destroyAction({ params }) {
await deleteContact(params.contactId);
return redirect("/");
}
并将它配置到路由中
javascript
...
import { action as destroyAction } from "./routes/destroy";
const router = createBrowserRouter([
{
path: "/",
...
children: [
...
{
path: "contacts/:contactId/destroy",
action: destroyAction,
},
],
},
]);
...
由于我们没有在子路由中配置 errorElement
,这是因为我们在destroyAction
中直接重定向到 " / ", 所以这个element
就没有创建的必要了。事实上也的确没有这个需求
javascript
// routerConfig.jsx
[
...
{
path: "contacts/:contactId/destroy",
action: destroyAction,
errorElement: <div>Oops! There was an error.</div>,
},
];
还有一点非常重要,Form 的 提交方法为 post 时才能激活路由中的action操作。
索引路由
每当我们加载这个App时,你会发现右侧是一个空页面。就像下面这样:
当一条路由有子路由时,而当前页面又处在父路由层级时,<Outlet>
没有任何子路由与之匹配,所以就没有可渲染的界面。这个时候可以将索引路由视为填充该空间的默认子路由。
创建 Index 组件如下所示
javascript
// 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>
);
}
配置这个Index到路由
javascript
// routerConfig.jsx
...
import Index from "./Index";
...
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <Error404 />,
loader: rootLoader,
action: rootAction,
children: [
{ index: true, element: <Index /> },
...
]
}
]);
export default router;
这个时候我们进入App重新渲染后界面如下:
完美。
现在还有最后一个功能没有完成,就是搜索功能,我们希望根据输入的关键词来搜索出联系人。
URL的搜索参数与Get提交
传统的html
表单中,如果没有指定提交方法的话默认为get
方式提交到服务器,如我们Root.jsx
中搜索框部分,这自然不是我们想要的结果,我们只是想把输入参数反应到地址栏中而不影响浏览器的变化,正好,Form
可以做到。我们把Root
中的 html
元素 form
改成 React Router
中的 Form
组件就好了,就像下面这样:
javascript
// Root.jsx
...
<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>
...
现在你的搜索栏中输入些内容回车后,你会发现浏览器的地址栏信息也会发会变化,但浏览器并没有网络请求操作。这正是我们目的,
现在我们修改 rootLoader()函数, 以获取get参数,并根据这个参数做出筛选联系人的反应:
javascript
// utils.jsx
...
// 模拟网络请求,获取数据
export async function rootLoader({request}) {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts };
}
...
现在就很美了。
再次强调:因为这是一个
get
的,而不是post
,React Router
路由器不调用action
。提交GET
表格与单击链接相同:只有URL更改。这就是为什么我们添加的过滤代码在loader中,而不是在 action 中。
这也意味着这是一个普通的页面导航。您可以单击"后退"按钮以返回到自己的位置。
最后
关于路由的主要应用技术我已经讲完了,根据这些应用方法,配置 React的状态管理,会使我们的应用更加灵活,设计更加方便。只要多练习,多思考,就一定能开发出非常完美的产品。