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 不会在水合时调用 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,然后在水合 (hydration) 时运行 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