客户端数据
此页面

客户端数据

Remix 在 v2.4.0 中引入了对“客户端数据”(RFC)的支持,允许您通过路由的 clientLoader/clientAction 导出选择在浏览器中运行路由加载器/操作。

这些新的导出有点像一把锋利的刀,不建议作为您主要的数据加载/提交机制 - 而是为您提供了一种杠杆,可以用来处理以下一些高级用例

  • 跳过跳转:使用加载器仅用于 SSR,直接从浏览器查询数据 API
  • 全栈状态:使用客户端数据增强服务器数据,以获得您所有加载器数据的完整集合
  • 择一:有时您使用服务器加载器,有时您使用客户端加载器,但同一路由不会同时使用两者
  • 客户端缓存:在客户端缓存服务器加载器数据并避免一些服务器调用
  • 迁移:简化从 React Router -> Remix SPA -> Remix SSR 的迁移(一旦 Remix 支持 SPA 模式

请谨慎使用这些新的导出!如果您不小心 - 很容易让您的 UI 不同步。默认情况下,Remix 非常努力地确保这种情况不会发生 - 但是一旦您控制了您自己的客户端缓存,并可能阻止 Remix 执行其正常的服务器 fetch 调用 - 那么 Remix 就无法再保证您的 UI 保持同步。

跳过跳转

在 BFF 架构中使用 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在初始加载时必须返回相同的数据,以避免hydration错误。

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