客户端数据
本页内容

客户端数据

您可以使用 clientLoaderclientAction 函数直接在浏览器中获取和修改数据。

当使用 SPA 模式时,这些函数是处理数据的主要机制。本指南演示了在服务器端渲染 (SSR) 中利用客户端数据的常见用例。

跳过服务器跳转

当使用 React Router 搭配 Backend-For-Frontend (BFF) 架构时,您可能希望绕过 React Router 服务器,直接与后端 API 进行通信。这种方法需要适当的身份验证处理,并假设没有 CORS 限制。以下是实现方法:

  1. 在文档加载时从服务器 loader 加载数据
  2. 在所有后续加载时从 clientLoader 加载数据

在这种情况下,React Router 在注水(hydration)时不会调用 clientLoader,而只会在后续导航时调用它。

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

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

全栈状态

有时您需要在渲染组件之前结合服务器和浏览器(如 IndexedDB 或浏览器 SDK)的数据。以下是实现此模式的方法:

  1. 在文档加载时从服务器 loader 加载部分数据
  2. 导出一个 HydrateFallback 组件,以便在 SSR 期间渲染,因为此时我们还没有完整的数据集
  3. 设置 clientLoader.hydrate = true,这将指示 React Router 在初始文档注水时调用 clientLoader
  4. clientLoader 中将服务器数据与客户端数据结合
export async function loader({
  request,
}: Route.LoaderArgs) {
  const partialData = await getPartialDataFromDb({
    request,
  }); // (1)
  return partialData;
}

export async function clientLoader({
  request,
  serverLoader,
}: Route.ClientLoaderArgs) {
  const [serverData, clientData] = await Promise.all([
    serverLoader(),
    getClientData(request),
  ]);
  return {
    ...serverData, // (4)
    ...clientData, // (4)
  };
}
clientLoader.hydrate = true as const; // (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
  loaderData,
}: Route.ComponentProps) {
  return <>...</>;
}

选择服务器或客户端数据加载

您可以在应用程序中混合使用数据加载策略,为每个路由选择仅服务器端加载或仅客户端加载数据。以下是实现这两种方法的方式:

  1. 当您想使用服务器数据时,导出一个 loader
  2. 当您想使用客户端数据时,导出 clientLoader 和一个 HydrateFallback

仅依赖服务器 loader 的路由如下所示

export async function loader({
  request,
}: Route.LoaderArgs) {
  const data = await getServerData(request);
  return data;
}

export default function Component({
  loaderData, // (1) - server data
}: Route.ComponentProps) {
  return <>...</>;
}

仅依赖客户端 loader 的路由如下所示。

export async function clientLoader({
  request,
}: Route.ClientLoaderArgs) {
  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({
  loaderData, // (2) - client data
}: Route.ComponentProps) {
  return <>...</>;
}

客户端缓存

您可以实现客户端缓存(使用内存、localStorage 等)来优化服务器请求。以下是一个演示缓存管理的模式:

  1. 在文档加载时从服务器 loader 加载数据
  2. 设置 clientLoader.hydrate = true 来预热缓存
  3. 通过 clientLoader 从缓存中加载后续导航
  4. 在您的 clientAction 中使缓存失效

请注意,由于我们没有导出一个 HydrateFallback 组件,我们将会 SSR 路由组件,然后在注水(hydration)时运行 clientLoader。因此,您的 loaderclientLoader 在初始加载时返回相同的数据非常重要,以避免注水错误。

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

export async function action({
  request,
}: Route.ActionArgs) {
  await saveDataToDb({ request });
  return { ok: true };
}

let isInitialRequest = true;

export async function clientLoader({
  request,
  serverLoader,
}: Route.ClientLoaderArgs) {
  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,
}: Route.ClientActionArgs) {
  const cacheKey = generateKey(request);
  cache.delete(cacheKey); // (4)
  const serverData = await serverAction();
  return serverData;
}
文档和示例 CC 4.0