加载数据
这一节主要强调 URL、布局和数据 的解耦。
在根模块文件中创建并导出一个加载器函数,并配置到路由。
getContacts() 是我们自己封装的数据请求 API,新增的数据暂时存储到 localforage。
export async function loader() {
const contacts = await getContacts();
return { contacts };
}
路由配置 loader
// 配置 loader
loader: rootLoader,
数据渲染,使用 内置的 useLoaderData() 方法
const { contacts } = useLoaderData();
数据写入
提交 HTML 表单时,它会在浏览器中引起导航事件,就像我们点击某个链接一样。而链接和提交表单的唯一的区别在于请求:链接只能更改 URL,而表单还可以更改请求方式(GET 与 POST)和请求体(POST 表单数据)。
如果没有客户端路由(也就是react-router
这类的client-side routing),浏览器会自动序列化表单数据,并以不同的方式发送给服务器。对于POST请求,数据会作为请求体(request body)发送给服务器;而对于GET请求,数据会作为URLSearchParams(URL查询参数)的形式附加在URL上发送给服务器。
React Router的行为与此类似,但它不会将请求发送到服务器,而是使用客户端路由并将请求发送到路由操作action
进行处理。
我们使用 React Router 封装的 <Form>
,创建用户。
<Form method="post">
<button type="submit">New</button>
</Form>
export async function action() {
const contact = await createContact();
return redirect(`/contacts/${contact.id}/edit`);
}
像 loader 一样,action 创建后也需要挂载在路由上。action: rootAction
。
可以简单的理解为 loader 是需要获取数据时触发的,action 则是修改数据时触发的。
创建用户信息,使用 Form submit之后,我们发现Reat Router 的<Form>
自动阻止了浏览器发送请求到服务器,而是将其发送到我们指定的路由操作action
中 。然后自动触发数据重新验证的过程,也就意味着所有使用 useLoaderData 钩子的组件都会自动更新!并且UI会自动与数据保持同步!避免我们使用 useState
、 onSubmit
和 useEffect
这些操作,极大地简化了我们的操作。
URL 参数
path: 'contacts/:contactId',
:contactId
URL 段。冒号 ( :
) 为“动态段”。动态段将匹配 URL 该位置上的动态(变化)值,如联系人 ID。我们将 URL 中的这些值称为“URL 参数”,简称 “params”。这些params
将作为键值对传递给加载器loader
,值将作为 params.contactId
传递(可以在 loader上使用)。
当然,每个组件中的 loader, action 等都需要在路由中进行配置,而且路由通常都有自己的 laoder 等,只不过他们有时可能会相同。
export async function loader({ params }) {
const contact = await getContact(params.contactId);
return { contact };
}
export async function Contact() {
const { contact } = useLoaderData();
// ...
}
更新数据
<input
placeholder="First"
aria-label="First name"
type="text"
name="first"
defaultValue={contact.first}
/>
如果没有 JavaScript,当提交表单时,浏览器会创建FormData
对象,并将其设置为请求的主体(body),然后将请求发送到服务器。
如前所述,React Router 阻止了这种默认行为,而是将请求发送到我们自己想要的操作,也就是自己在组件中定义的 action
中,同时包括FormData
。表单中的每个字段都可以通过 formData.get(name)
访问。
这里还使用Object.fromEntries
将数据全部收集到一个对象中,这正是后面自定义的 updateContact
函数想要的。
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}`);
}
loader
和action
都可以返回Response
(因为它们都收到了Request
!)。
redirect
辅助函数只是为了更方便地返回response
,告诉应用程序更改位置。
活动链接样式
<NavLink
to={`contacts/${contact.id}`}
className={({ isActive, isPending }) =>
isActive
? "active"
: isPending
? "pending"
: ""
}
>
<NavLink>
是一种特殊的 <Link>
,它知道自己是否处于 “激活”、"待定 "或 "过渡 "状态。当用户访处于NavLink指定的URL时,isActive将为true。当链接即将被激活时(数据仍在加载中), isPending
将为 true。
loading 界面
当用户浏览应用时,React Router 会在为下一页加载数据时保留旧页面。也就是说这种数据模型具有客户端缓存,因此第二次导航到之前已经访问过的路由时速度会很快。
useNavigation
返回当前导航状态:可以是"idle" | "submitting" | "loading"
const navigation = useNavigation();
// ...
navigation.state === "loading" ? "loading" : ""
删除数据
<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>
action
指向 "destroy"
。与 <Link to>
一样, <Form action>
也可以接收一个相对值。由于表单是在 contact/:contactId
中呈现的,因此点击 destroy
的相对操作将把表单提交到 contact/:contactId/destroy
,所以我们只需要在 contact/:contactId/destroy
路由下指定删除操作即可。比如:
export async function action({ params }) {
await deleteContact(params.contactId);
return redirect("/");
}
{
path: 'contacts/:contactId/destroy',
action: destroyAction,
},
详细说明,当用户点击提交按钮时:
<Form>
会阻止浏览器向服务器发送新 POST 请求的默认行为,而是通过客户端路由创建一个 POST 请求来模拟浏览器的行为<Form action="destroy">
匹配道路新路由"contacts/:contactId/destroy"
,并向其发送请求- 在操作重定向后,React Router 会调用页面上所有数据的
loader
,以获取最新值(这就是 “重新验证”)。useLoaderData
返回新值,并导致组件更新!
捕获错误
export async function action({ params }) {
await deleteContact(params.contactId);
throw new Error("oh dang!");
return redirect("/");
}
虽然我这个操作中 return redirect("/");
根本就不会执行,但这只是为了说明如何捕获错误😁。
这样操作完成之后,发现直接如今了错误页,必须刷新才会重新加载恢复正常显示。
因此需要在路由中进行捕获:
{
path: 'contacts/:contactId/destroy',
action: destroyAction,
errorElement: <div>Oops! There was an error.</div>,
},
因为销毁路由有自己的errorElement
,并且是根路由的子路由,因此错误会在此处路由而不是根路由上呈现。这些错误会以最近的 errorElement
冒泡。只要在根路径上有一个,添加多少都行。
索引路由
如果在父路由的路径上, <Outlet>
由于没有子路由匹配,所以没有任何内容可以呈现。可以把索引路由看作是填补这一空白的默认子路由。
定义一个索引路由的页面模块 index。然后在子路由上添加:
{ index: true, element: <Index /> },
这将告诉路由,当用户位于父路由的确切路径时,路由器将匹配并呈现此路由,以保证在 <Outlet>
中没有其他子路由需要呈现时页面不为空。
导航
用一个取消功能来说明。
const navigate = useNavigate();
<button
type="button"
onClick={() => {
navigate(-1);
}}
>Cancel</button>
这里的 <button type="button">
虽然看似多余,却是防止按钮提交表单的 默认 HTML 行为。所以按钮上没有event.preventDefault
。
以上说明中路由:
// 创建路由
const router = createBrowserRouter([
{
path: '/',
// 将<Root>设置为根路由element
element: <Root/>,
// 将<ErrorPage>设置为根路由上的errorElement
errorElement: <ErrorPage/>,
// 配置 loader
loader: rootLoader,
// 配置 action
action: rootAction,
// 子路由
children: [
{ index: true, element: <Index /> },
{
path: 'contacts/:contactId',
element: <Contact/>,
loader: contactLoader
},
{
path: "contacts/:contactId/edit",
element: <EditContact/>,
loader: contactLoader,
action: editAction
},
{
path: 'contacts/:contactId/destroy',
action: destroyAction,
errorElement: <div>Oops! There was an error.</div>,
},
]
},
])