想像一個場景,你的某條路線的載入器需要擷取一些資料,而這些資料由於某種原因而非常慢。例如,假設你向使用者顯示包裹運送至其住家的位置
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
平行載入資料(我們的範例中沒有要平行的部分,但在其他情況下可能有點幫助)。如果這些方法無法正常運作,您可能會被迫將慢速資料從 loader
移到元件的提取中(並在載入時顯示一個概觀暫停畫面 UI)。這種情況下,您會在載入時顯示暫停畫面 UI,並觸發資料提取。多虧了 useFetcher
,這其實在 DX 方面並沒有那麼糟。而且從 UX 的角度而言,這改善了用戶端轉換以及初始頁面載入的載入體驗。因此看起來可以解決問題。
但大多數情況下仍然次佳(特別是在進行路由元件的區塊分割時),原因有兩個
React Router 運用 React 18 的 Suspense 來提取資料,使用 defer
回應 工具程式和 <Await />
元件/ useAsyncValue
鉤子。透過使用這些 API,您可以解決這兩個問題
讓我們深入探討如何達成這項工作。
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>
);
}
如果您並不想回歸呈現 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% 僅與路由及其參數的初始載入有關。
當您使用遞延
時,您會告訴 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()),
});
}
或考慮我們延遲資料可能會傳回重新導向的回應
的情境。您可以偵測重新導向並將狀態碼和位置送回作為資料,然後您可以透過useEffect
和useNavigate
在您的元件中進行用戶端重新導向。
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 });
}