React Router v7 已發布。 查看文件
客戶端資料
本頁內容

客戶端資料

Remix 在 v2.4.0 中引入了對「客戶端資料」的支援(RFC),這讓您可以選擇透過路由中的 clientLoader/clientAction 匯出,在瀏覽器中執行路由載入器/動作。

這些新的匯出功能是一把鋒利的刀,不建議作為您的主要資料載入/提交機制,而是讓您可以在以下某些進階用例中加以運用

  • 跳過中繼點: 直接從瀏覽器查詢資料 API,僅使用載入器進行伺服器端渲染 (SSR)
  • 全堆疊狀態: 使用客戶端資料擴充伺服器資料,以取得完整的載入器資料集
  • 二選一: 有時您使用伺服器載入器,有時您使用客戶端載入器,但不會在一個路由上同時使用兩者
  • 客戶端快取: 在客戶端快取伺服器載入器資料,並避免某些伺服器呼叫
  • 遷移: 簡化從 React Router -> Remix SPA -> Remix SSR 的遷移(一旦 Remix 支援 SPA 模式

請謹慎使用這些新的匯出功能!如果您不小心,很容易讓您的 UI 失去同步。Remix 開箱即用,會非常努力確保這種情況不會發生,但是一旦您控制了自己的客戶端快取,並可能阻止 Remix 執行其正常的伺服器 fetch 呼叫,那麼 Remix 將無法再保證您的 UI 保持同步。

跳過中繼點

當在 BFF 架構中使用 Remix 時,跳過 Remix 伺服器中繼點並直接存取您的後端 API 可能會很有利。這假設您可以適當地處理身份驗證,並且不受 CORS 問題的影響。您可以如下跳過 Remix BFF 中繼點

  1. 在文件載入時從伺服器 loader 載入資料
  2. 在所有後續載入時從 clientLoader 載入資料

在這種情況下,Remix 不會在水合 (hydration) 時呼叫 clientLoader,而只會在後續導覽時呼叫它。

import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import type { ClientLoaderFunctionArgs } from "@remix-run/react";

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const data = await fetchApiFromServer({ request }); // (1)
  return json(data);
}

export async function clientLoader({
  request,
}: ClientLoaderFunctionArgs) {
  const data = await fetchApiFromClient({ request }); // (2)
  return data;
}

全堆疊狀態

有時,您可能想要利用「全堆疊狀態」,其中某些資料來自伺服器,而某些資料來自瀏覽器(即 IndexedDB 或其他瀏覽器 SDK),但在您擁有完整的資料集之前,無法呈現您的元件。您可以如下組合這兩個資料來源

  1. 在文件載入時從伺服器 loader 載入部分資料
  2. 匯出一個 HydrateFallback 元件,以便在 SSR 期間呈現,因為我們尚未擁有完整的資料集
  3. 設定 clientLoader.hydrate = true,這會指示 Remix 在初始文件水合期間呼叫 clientLoader
  4. clientLoader 中組合伺服器資料和客戶端資料
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import type { ClientLoaderFunctionArgs } from "@remix-run/react";

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const partialData = await getPartialDataFromDb({
    request,
  }); // (1)
  return json(partialData);
}

export async function clientLoader({
  request,
  serverLoader,
}: ClientLoaderFunctionArgs) {
  const [serverData, clientData] = await Promise.all([
    serverLoader(),
    getClientData(request),
  ]);
  return {
    ...serverData, // (4)
    ...clientData, // (4)
  };
}
clientLoader.hydrate = true; // (3)

export function HydrateFallback() {
  return <p>Skeleton rendered during SSR</p>; // (2)
}

export default function Component() {
  // This will always be the combined set of server + client data
  const data = useLoaderData();
  return <>...</>;
}

二選一

您可能想要在您的應用程式中混合搭配資料載入策略,以便某些路由僅在伺服器上載入資料,而某些路由僅在客戶端上載入資料。您可以如下針對每個路由進行選擇

  1. 當您想要使用伺服器資料時,匯出一個 loader
  2. 當您想要使用客戶端資料時,匯出 clientLoaderHydrateFallback

僅依賴伺服器載入器的路由如下所示

import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const data = await getServerData(request);
  return json(data);
}

export default function Component() {
  const data = useLoaderData(); // (1) - server data
  return <>...</>;
}

僅依賴客戶端載入器的路由如下所示。

import type { ClientLoaderFunctionArgs } from "@remix-run/react";

export async function clientLoader({
  request,
}: ClientLoaderFunctionArgs) {
  const clientData = await getClientData(request);
  return clientData;
}
// Note: you do not have to set this explicitly - it is implied if there is no `loader`
clientLoader.hydrate = true;

// (2)
export function HydrateFallback() {
  return <p>Skeleton rendered during SSR</p>;
}

export default function Component() {
  const data = useLoaderData(); // (2) - client data
  return <>...</>;
}

客戶端快取

您可以利用客戶端快取(記憶體、本機儲存空間等)來繞過某些伺服器呼叫,如下所示

  1. 在文件載入時從伺服器 loader 載入資料
  2. 設定 clientLoader.hydrate = true 以初始化快取
  3. 透過 clientLoader 從快取載入後續導覽
  4. 在您的 clientAction 中使快取失效

請注意,由於我們沒有匯出 HydrateFallback 元件,我們將對路由元件進行 SSR,然後在水合時執行 clientLoader,因此您的 loaderclientLoader 在初始載入時傳回相同的資料,以避免水合錯誤,這點非常重要。

import type {
  ActionFunctionArgs,
  LoaderFunctionArgs,
} from "@remix-run/node";
import { json } from "@remix-run/node";
import type {
  ClientActionFunctionArgs,
  ClientLoaderFunctionArgs,
} from "@remix-run/react";

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const data = await getDataFromDb({ request }); // (1)
  return json(data);
}

export async function action({
  request,
}: ActionFunctionArgs) {
  await saveDataToDb({ request });
  return json({ ok: true });
}

let isInitialRequest = true;

export async function clientLoader({
  request,
  serverLoader,
}: ClientLoaderFunctionArgs) {
  const cacheKey = generateKey(request);

  if (isInitialRequest) {
    isInitialRequest = false;
    const serverData = await serverLoader();
    cache.set(cacheKey, serverData); // (2)
    return serverData;
  }

  const cachedData = await cache.get(cacheKey);
  if (cachedData) {
    return cachedData; // (3)
  }

  const serverData = await serverLoader();
  cache.set(cacheKey, serverData);
  return serverData;
}
clientLoader.hydrate = true; // (2)

export async function clientAction({
  request,
  serverAction,
}: ClientActionFunctionArgs) {
  const cacheKey = generateKey(request);
  cache.delete(cacheKey); // (4)
  const serverData = await serverAction();
  return serverData;
}

遷移

我們預計在 SPA 模式推出後,撰寫一份單獨的遷移指南,但目前我們預計此過程會像這樣

  1. 透過移至 createBrowserRouter/RouterProvider,在您的 React Router SPA 中引入資料模式
  2. 將您的 SPA 移至使用 Vite,以便更好地為 Remix 遷移做準備
  3. 透過使用 Vite 外掛程式(尚未提供)逐步移至基於檔案的路由定義
  4. 將您的 React Router SPA 遷移至 Remix SPA 模式,其中所有目前基於檔案的 loader 函數都充當 clientLoader
  5. 選擇退出 Remix SPA 模式(並進入 Remix SSR 模式),並尋找/取代您的 loader 函數為 clientLoader
    • 您現在正在執行 SSR 應用程式,但您所有的資料載入仍然透過 clientLoader 在客戶端中進行
  6. 逐步開始移動 clientLoader -> loader,以開始將資料載入移至伺服器
文件和範例授權採用 MIT