【React Router】初识路由(下)

发布于:2024-04-19 ⋅ 阅读:(26) ⋅ 点赞:(0)

Form

普通搜索框的表单提交

<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,使用客户端路由来提交此表单,并可以在现有的加载器中对数据进行操作。

比如:

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 发生了变化。

优化表单

  1. 如果在搜索后刷新页面,表单字段中应保留输入的值
    解决:设置 q 为默认值(q是我们自定义的表单某项的 key)
const { contacts, q } = useLoaderData();//使用q
<input
    id="q"
    className={searching ? "loading" : ""}
    aria-label="Search contacts"
    placeholder="Search"
    type="search"
    name="q"
    defaultValue={q}
/>
  1. 在搜索后点击返回,列表已不再过滤,表单字段需要清空输入的值
useEffect(() => {
    document.getElementById("q").value = q;
}, [q]);

当然这里同时使用 useEffect, useState 也是可以的

  1. 在每次按键时进行过滤,而不是在表单明确提交时进行筛选

使用 React Router 自带的 useSubmit 。

const submit = useSubmit();
<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 函数将序列化并提交传递给它的任何表单。
4. 由于每次按键都会提交表单,所以如果我们输入 "seba "字符,然后用退格键删除它们,历史堆栈中就会出现 7 个新条目😂(长按回退按钮可以查看)。通过将历史记录堆栈中的当前条目替换为下一页,而不是推入下一页,来避免这种情况。

<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钩子函数。它允许我们与loadersactions进行通信,而不会导致导航。

const fetcher = useFetcher();
<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="..."> 属性,它将提交到呈现表单的路由。

export async function action({ request, params }) {
    let formData = await request.formData();
    return updateContact(params.contactId, {
        favorite: formData.get("favorite") === "true",
    });
}

然后在路由中配置 action 即可。

局部捕获错误异常

export async function loader({ params }) {
    const contact = await getContact(params.contactId);
    if (!contact) {
        throw new Response("", {
            status: 404,
            statusText: "Not Found",
        });
    }
    return { contact };
}

这样操作完成之后,我们发现异常页是全局的,所以就需要在路由中配置,将子路由包裹在无路径路由中变成局部异常页。

{
    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>,
                },
            ]
        }
    ]
},