使用 Fetcher
本页内容

使用 Fetcher

Fetchers 对于创建复杂的、动态的用户界面非常有用,这些界面需要多个并发的数据交互而无需引起页面导航。

Fetchers 追踪它们自己的独立状态,可以用来加载数据、修改数据、提交表单,以及通常与 loaders 和 actions 进行交互。

调用 Actions

fetcher 最常见的用例是将数据提交给一个 action,从而触发路由数据的重新验证 (revalidation)。考虑以下路由模块:

import { useLoaderData } from "react-router";

export async function clientLoader({ request }) {
  let title = localStorage.getItem("title") || "No Title";
  return { title };
}

export default function Component() {
  let data = useLoaderData();
  return (
    <div>
      <h1>{data.title}</h1>
    </div>
  );
}

1. 添加一个 action

首先,我们将向路由添加一个供 fetcher 调用的 action。

import { useLoaderData } from "react-router";

export async function clientLoader({ request }) {
  // ...
}

export async function clientAction({ request }) {
  await new Promise((res) => setTimeout(res, 1000));
  let data = await request.formData();
  localStorage.setItem("title", data.get("title"));
  return { ok: true };
}

export default function Component() {
  let data = useLoaderData();
  // ...
}

2. 创建一个 fetcher

接下来创建一个 fetcher 并使用它渲染一个表单。

import { useLoaderData, useFetcher } from "react-router";

// ...

export default function Component() {
  let data = useLoaderData();
  let fetcher = useFetcher();
  return (
    <div>
      <h1>{data.title}</h1>

      <fetcher.Form method="post">
        <input type="text" name="title" />
      </fetcher.Form>
    </div>
  );
}

3. 提交表单

如果你现在提交表单,fetcher 将调用 action 并自动重新验证路由数据。

4. 渲染等待状态

Fetchers 在异步工作期间使其状态可用,因此你可以在用户交互时立即渲染等待状态的 UI。

export default function Component() {
  let data = useLoaderData();
  let fetcher = useFetcher();
  return (
    <div>
      <h1>{data.title}</h1>

      <fetcher.Form method="post">
        <input type="text" name="title" />
        {fetcher.state !== "idle" && <p>Saving...</p>}
      </fetcher.Form>
    </div>
  );
}

5. 乐观 UI

有时表单中有足够的信息可以直接渲染下一个状态。你可以通过 fetcher.formData 访问表单数据。

export default function Component() {
  let data = useLoaderData();
  let fetcher = useFetcher();
  let title = fetcher.formData?.get("title") || data.title;

  return (
    <div>
      <h1>{title}</h1>

      <fetcher.Form method="post">
        <input type="text" name="title" />
        {fetcher.state !== "idle" && <p>Saving...</p>}
      </fetcher.Form>
    </div>
  );
}

6. Fetcher 数据和验证

从 action 返回的数据可在 fetcher 的 data 属性中获取。这主要用于在修改失败时向用户返回错误消息。

// ...

export async function clientAction({ request }) {
  await new Promise((res) => setTimeout(res, 1000));
  let data = await request.formData();

  let title = data.get("title") as string;
  if (title.trim() === "") {
    return { ok: false, error: "Title cannot be empty" };
  }

  localStorage.setItem("title", title);
  return { ok: true, error: null };
}

export default function Component() {
  let data = useLoaderData();
  let fetcher = useFetcher();
  let title = fetcher.formData?.get("title") || data.title;

  return (
    <div>
      <h1>{title}</h1>

      <fetcher.Form method="post">
        <input type="text" name="title" />
        {fetcher.state !== "idle" && <p>Saving...</p>}
        {fetcher.data?.error && (
          <p style={{ color: "red" }}>
            {fetcher.data.error}
          </p>
        )}
      </fetcher.Form>
    </div>
  );
}

加载数据

fetcher 的另一个常见用例是从路由加载数据,用于像组合框 (combobox) 这样的组件。

1. 创建搜索路由

考虑以下包含一个非常基础的搜索功能的路由:

// { path: '/search-users', filename: './search-users.tsx' }
const users = [
  { id: 1, name: "Ryan" },
  { id: 2, name: "Michael" },
  // ...
];

export async function loader({ request }) {
  await new Promise((res) => setTimeout(res, 300));
  let url = new URL(request.url);
  let query = url.searchParams.get("q");
  return users.filter((user) =>
    user.name.toLowerCase().includes(query.toLowerCase())
  );
}

2. 在组合框组件中渲染一个 fetcher

import { useFetcher } from "react-router";

export function UserSearchCombobox() {
  let fetcher = useFetcher();
  return (
    <div>
      <fetcher.Form method="get" action="/search-users">
        <input type="text" name="q" />
      </fetcher.Form>
    </div>
  );
}
  • action 指向我们上面创建的路由:"/search-users"。
  • input 的名称是 "q",以匹配查询参数。

3. 添加类型推断

import { useFetcher } from "react-router";
import type { Search } from "./search-users";

export function UserSearchCombobox() {
  let fetcher = useFetcher<typeof Search.action>();
  // ...
}

确保使用 import type,这样你只导入类型。

4. 渲染数据

import { useFetcher } from "react-router";

export function UserSearchCombobox() {
  let fetcher = useFetcher<typeof Search.action>();
  return (
    <div>
      <fetcher.Form method="get" action="/search-users">
        <input type="text" name="q" />
      </fetcher.Form>
      {fetcher.data && (
        <ul>
          {fetcher.data.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

注意,你需要按下 "enter" 键来提交表单并查看结果。

5. 渲染等待状态

import { useFetcher } from "react-router";

export function UserSearchCombobox() {
  let fetcher = useFetcher<typeof Search.action>();
  return (
    <div>
      <fetcher.Form method="get" action="/search-users">
        <input type="text" name="q" />
      </fetcher.Form>
      {fetcher.data && (
        <ul
          style={{
            opacity: fetcher.state === "idle" ? 1 : 0.25,
          }}
        >
          {fetcher.data.map((user) => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

6. 根据用户输入进行搜索

Fetchers 可以使用 fetcher.submit 进行编程方式提交。

<fetcher.Form method="get" action="/search-users">
  <input
    type="text"
    name="q"
    onChange={(event) => {
      fetcher.submit(event.currentTarget.form);
    }}
  />
</fetcher.Form>

注意,input 事件的 form 会作为第一个参数传递给 fetcher.submit。fetcher 将使用该 form 来提交请求,读取其属性并序列化其元素中的数据。

文档和示例 CC 4.0