主要
分支
主要版本 (6.23.1)開發版本
版本
6.23.1v4/5.xv3.x
延遲載入資料
本頁內容

遞延資料指南

問題

想像一個場景,你的某條路線的載入器需要擷取一些資料,而這些資料由於某種原因而非常慢。例如,假設你向使用者顯示包裹運送至其住家的位置

import { json, useLoaderData } from "react-router-dom";
import { getPackageLocation } from "./api/packages";

async function loader({ params }) {
  const packageLocation = await getPackageLocation(
    params.packageId
  );

  return json({ packageLocation });
}

function PackageRoute() {
  const data = useLoaderData();
  const { packageLocation } = data;

  return (
    <main>
      <h1>Let's locate your package</h1>
      <p>
        Your package is at {packageLocation.latitude} lat
        and {packageLocation.longitude} long.
      </p>
    </main>
  );
}

我們假設 getPackageLocation rất chậm。這會導致初始頁面載入時間和轉換至該路線的時間與最慢的資料片段一樣長。有幾種方法可以最佳化這一點並改善使用者體驗

  • 加快慢的部分 (😅)。
  • 使用 Promise.all 平行載入資料(我們的範例中沒有要平行的部分,但在其他情況下可能有點幫助)。
  • 加入全域轉換指示條(有助於提升 UX)。
  • 加入區域化結構式虛擬使用者介面(有助於提升 UX)。

如果這些方法無法正常運作,您可能會被迫將慢速資料從 loader 移到元件的提取中(並在載入時顯示一個概觀暫停畫面 UI)。這種情況下,您會在載入時顯示暫停畫面 UI,並觸發資料提取。多虧了 useFetcher,這其實在 DX 方面並沒有那麼糟。而且從 UX 的角度而言,這改善了用戶端轉換以及初始頁面載入的載入體驗。因此看起來可以解決問題。

但大多數情況下仍然次佳(特別是在進行路由元件的區塊分割時),原因有兩個

  1. 用戶端提取讓您的資料要求處於瀑布式:文件 -> JavaScript -> 延遲載入的路由 -> 資料提取
  2. 您的程式碼無法輕易地在元件提取和路由提取之間切換(稍後會詳細說明)。

解決方案

React Router 運用 React 18 的 Suspense 來提取資料,使用 defer 回應 工具程式和 <Await /> 元件/ useAsyncValue 鉤子。透過使用這些 API,您可以解決這兩個問題

  1. 您的資料不再位於瀑布式中:文件 -> JavaScript -> 延遲載入的路由與資料(平行)
  2. 您的程式碼可以輕鬆地在顯示暫停畫面和等候資料之間切換

讓我們深入探討如何達成這項工作。

使用 defer

首先針對您的慢速資料要求加上 <Await />,這會優先顯示暫停畫面 UI。讓我們針對上述範例執行這個動作

import {
  Await,
  defer,
  useLoaderData,
} from "react-router-dom";
import { getPackageLocation } from "./api/packages";

async function loader({ params }) {
  const packageLocationPromise = getPackageLocation(
    params.packageId
  );

  return defer({
    packageLocation: packageLocationPromise,
  });
}

export default function PackageRoute() {
  const data = useLoaderData();

  return (
    <main>
      <h1>Let's locate your package</h1>
      <React.Suspense
        fallback={<p>Loading package location...</p>}
      >
        <Await
          resolve={data.packageLocation}
          errorElement={
            <p>Error loading package location!</p>
          }
        >
          {(packageLocation) => (
            <p>
              Your package is at {packageLocation.latitude}{" "}
              lat and {packageLocation.longitude} long.
            </p>
          )}
        </Await>
      </React.Suspense>
    </main>
  );
}
或者,您可以使用 `useAsyncValue` 鉤子

如果您並不想回歸呈現 props,您可以使用鉤子,但您必須將內容區分到另一個元件

export default function PackageRoute() {
  const data = useLoaderData();

  return (
    <main>
      <h1>Let's locate your package</h1>
      <React.Suspense
        fallback={<p>Loading package location...</p>}
      >
        <Await
          resolve={data.packageLocation}
          errorElement={
            <p>Error loading package location!</p>
          }
        >
          <PackageLocation />
        </Await>
      </React.Suspense>
    </main>
  );
}

function PackageLocation() {
  const packageLocation = useAsyncValue();
  return (
    <p>
      Your package is at {packageLocation.latitude} lat and{" "}
      {packageLocation.longitude} long.
    </p>
  );
}

評估解決方案

因此,我們不是等到元件載入後再觸發提取要求,而是在使用者開始轉換到新的路由後就啟動對慢速資料的要求。這可以大幅加速使用者在較慢網路中的使用體驗。

此外,React Router 為此公開的 API 非常符合人體工學。您可以根據是否包含 await 關鍵字,在準備遞延和不遞延之間直接切換

return defer({
  // not deferred:
  packageLocation: await packageLocationPromise,
  // deferred:
  packageLocation: packageLocationPromise,
});

因此,您可以 A/B 測試遞延,甚至根據使用者或被要求的資料來決定是否遞延

async function loader({ request, params }) {
  const packageLocationPromise = getPackageLocation(
    params.packageId
  );
  const shouldDefer = shouldDeferPackageLocation(
    request,
    params.packageId
  );

  return defer({
    packageLocation: shouldDefer
      ? packageLocationPromise
      : await packageLocationPromise,
  });
}

這個 shouldDeferPackageLocation 可以實作為檢查發出請求的使用者、包裹位置資料是否在快取中、A/B 測試的狀態,或任何您想要的其他檢查。這真的很棒 🍭

常見問答

為什麼不預設遞延所有內容?

React Router 的遞延 API 是 React Router 提供的另一個槓桿,可讓您以友善的方式在各種折衷方案之間做出選擇。您希望頁面更快速的渲染嗎?遞延相關內容。您希望 CLS(內容佈局位移)較低嗎?不遞延相關內容。您希望渲染速度加快,但也希望 CLS 降低嗎?僅遞延低速度且不重要的內容。

以上皆為折衷方案,API 設計的巧妙之處在於它非常適合您輕鬆進行實驗,找出哪一種折衷方案可以為您的實際世界關鍵指標帶來更好的結果。

什麼時候<Suspense/>回退渲染?

<Await />元件只會在<Suspend>邊界中,有未確定承諾的<Await />元件的初始呈現中,拋出承諾。假設屬性變更,它不會重新呈現回退。實際上,這表示當使用者提交表單且載入程式資料重新驗證時,您將不會取得回退呈現。當使用者帶著不同的參數導覽至同一路由(以我們上述的範例為例,如果使用者從左方的套件清單中選取,以在右邊找到其位置)時,您將會取得回退呈現。

一開始這也許感覺違反直覺,但請與我們保持聯繫,我們真的仔細考慮過了,以這種方式運作很重要。讓我們想像一個沒有遞延 API 的世界。對於那些場景,您可能想為表單提交/重新驗證實作樂觀的 UI。

當您決定嘗試遞延的折衷方案時,我們不希望您必須變更或移除那些最佳化,因為我們希望您能輕鬆在遞延部分資料和不遞延部分資料之間進行切換。因此,我們會確保您現有的樂觀狀態以相同方式運作。假設我們沒有這麼做,那麼您可能會體驗到我們所稱的「爆米花 UI」,其中提交資料會觸發回退載入狀態,而不是您努力實現的樂觀 UI。

因此,請謹記這點:遞延 100% 僅與路由及其參數的初始載入有關。

載入器回傳的 Response 物件為什麼不再運作了?

當您使用遞延時,您會告訴 React Router 立即載入頁面,且不含遞延資料。在Response物件回傳前頁面已經載入完畢,因此回應不會自動處理,就像您執行return fetch(url)一樣。

因此,您需要處理自己的Response處理程序,並以資料(而非Response執行個體)解析遞延 Promise。

async function loader({ request, params }) {
  return defer({
    // Broken! Resolves with a Response
    // broken: fetch(url),

    // Fixed! Resolves with the response data
    data: fetch(url).then((res) => res.json()),
  });
}

或考慮我們延遲資料可能會傳回重新導向的回應的情境。您可以偵測重新導向並將狀態碼和位置送回作為資料,然後您可以透過useEffectuseNavigate在您的元件中進行用戶端重新導向。

async function loader({ request, params }) {
  let data = fetch(url).then((res) => {
    if (res.status == 301) {
      return {
        isRedirect: true,
        status: res.status,
        location: res.headers.get("Location"),
      };
    }
    return res.json();
  });

  return defer({ data });
}