使用 Fetchers
本頁面內容

使用 Fetchers

Fetcher 非常適合用於建立複雜、動態的使用者介面,這些介面需要多次並行資料互動,而不會導致導航。

Fetcher 會追蹤其自身獨立的狀態,可用於載入資料、變更資料、提交表單,以及大致上與載入器和動作互動。

呼叫動作

Fetcher 最常見的用途是將資料提交至動作,以觸發路由資料的重新驗證。請考慮下列路由模組

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. 新增動作

首先,我們將新增一個動作到路由,供 fetcher 呼叫

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 將會呼叫動作並自動重新驗證路由資料。

4. 渲染待處理狀態

Fetcher 在非同步工作期間使其狀態可用,讓您可以在使用者互動時立即渲染待處理 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 資料與驗證

從動作傳回的資料可在 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 的另一個常見用途是從路由載入資料,用於下拉式方塊之類的功能。

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>
  );
}
  • 動作指向我們在上方建立的路由:「/search-users」
  • 輸入的名稱為 "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. 依使用者輸入搜尋

Fetcher 可以透過程式設計方式使用 fetcher.submit 提交

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

請注意,輸入事件的表單會以第一個引數傳遞至 fetcher.submit。Fetcher 將使用該表單提交請求,讀取其屬性並序列化其元素中的資料。

文件與範例 CC 4.0