Form
普通搜索框的表单提交
jsx
<form id="search-form" role="search">
<input
id="q"
className={searching ? "loading" : ""}
aria-label="Search contacts"
placeholder="Search"
type="search"
// URL 中有 `?q=`
name="q"
/>
<div
id="search-spinner"
hidden={!searching}
aria-hidden
/>
<div
className="sr-only"
aria-live="polite"
></div>
</form>
form 默认为 get 请求,将表单数据放入get 请求的URLSearchParams
中。如果 method 设为 post,那么将放入 post 主体中。
Form 表单提交
将 form 改为 Form,使用客户端路由来提交此表单,并可以在现有的加载器中对数据进行操作。
比如:
jsx
export async function loader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts, q };
}
因为是 GET 请求而不是 POST 请求,所以 React Router 不会调用 action
。
提交 GET 表单与点击链接一样:只是 URL 发生了变化。
优化表单
- 如果在搜索后刷新页面,表单字段中应保留输入的值
解决:设置 q 为默认值(q是我们自定义的表单某项的 key)
jsx
const { contacts, q } = useLoaderData();//使用q
jsx
<input
id="q"
className={searching ? "loading" : ""}
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
defaultValue={q}
/>
- 在搜索后点击返回,列表已不再过滤,表单字段需要清空输入的值
jsx
useEffect(() => {
document.getElementById("q").value = q;
}, [q]);
当然这里同时使用 useEffect, useState 也是可以的
- 在每次按键时进行过滤,而不是在表单明确提交时进行筛选
使用 React Router 自带的 useSubmit
。
jsx
const submit = useSubmit();
jsx
<input
id="q"
className={searching ? "loading" : ""}
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
defaultValue={q}
onChange={(event) => {
submit(event.currentTarget.form);
}}
/>
现在,当输入时,表格就会自动提交。
currentTarget
是事件附加到的 DOM 节点, currentTarget.form
是输入的父表单节点。submit
函数将序列化并提交传递给它的任何表单。
- 由于每次按键都会提交表单,所以如果我们输入 "seba "字符,然后用退格键删除它们,历史堆栈中就会出现 7 个新条目😂(长按回退按钮可以查看)。通过将历史记录堆栈中的当前条目替换为下一页,而不是推入下一页,来避免这种情况。
jsx
<input
id="q"
className={searching ? "loading" : ""}
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
defaultValue={q}
onChange={(event) => {
//修改:添加replace
const isFirstSearch = q == null;
submit(event.currentTarget.form, {
replace: !isFirstSearch,
});
}}
/>
非导航的数据改变
之前的所有的更改数据都是使用表单导航,在历史堆栈中创建新条目。
如果我们不希望引起导航更改数据,可以使用useFetcher
钩子函数。它允许我们与loaders
和actions
进行通信,而不会导致导航。
jsx
const fetcher = useFetcher();
jsx
<fetcher.Form method="post">
<button
name="favorite"
value={favorite ? "false" : "true"}
aria-label={
favorite
? "Remove from favorites"
: "Add to favorites"
}
>
{favorite ? "★" : "☆"}
</button>
</fetcher.Form>
有 method="post"
,它就会调用action
。由于没有提供 <fetcher.Form action="...">
属性,它将提交到呈现表单的路由。
jsx
export async function action({ request, params }) {
let formData = await request.formData();
return updateContact(params.contactId, {
favorite: formData.get("favorite") === "true",
});
}
然后在路由中配置 action 即可。
局部捕获错误异常
jsx
export async function loader({ params }) {
const contact = await getContact(params.contactId);
if (!contact) {
throw new Response("", {
status: 404,
statusText: "Not Found",
});
}
return { contact };
}
这样操作完成之后,我们发现异常页是全局的,所以就需要在路由中配置,将子路由包裹在无路径路由中变成局部异常页。
jsx
{
path: '/',
// 将<Root>设置为根路由element
element: <Root/>,
// 将<ErrorPage>设置为根路由上的errorElement
errorElement: <ErrorPage/>,
// 配置 loader
loader: rootLoader,
// 配置 action
action: rootAction,
// 子路由
children: [
// 添加无路径路由
{
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Index /> },
{
path: 'contacts/:contactId',
element: <Contact/>,
loader: contactLoader,
action: contactAction,
},
{
path: "contacts/:contactId/edit",
element: <EditContact/>,
loader: contactLoader,
action: editAction
},
{
path: 'contacts/:contactId/destroy',
action: destroyAction,
errorElement: <div>Oops! There was an error.</div>,
},
]
}
]
},