【React Router】初识路由(中)

加载数据

这一节主要强调 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}`);
}

loaderaction都可以返回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,
},

详细说明,当用户点击提交按钮时:

  1. <Form> 会阻止浏览器向服务器发送新 POST 请求的默认行为,而是通过客户端路由创建一个 POST 请求来模拟浏览器的行为
  2. <Form action="destroy"> 匹配道路新路由 "contacts/:contactId/destroy" ,并向其发送请求
  3. 在操作重定向后,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>,
            },
        ]
    },
])
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秀秀_heo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值