跳转到内容

客户端数据

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

这些新的导出功能有点棘手,不建议将其作为您的主要数据加载 / 提交机制 - 但可以为您提供一些可用于以下一些高级用例的杠杆:

  • 跳过跳转:直接从浏览器查询数据 API,使用加载器简单地进行 SSR
  • 全栈状态:使用客户端数据扩充服务器数据以获得完整的加载器数据集
  • 二选一:有时使用服务器加载器,有时使用客户端加载器,但不能在一条路由上同时使用两者
  • 客户端缓存:在客户端缓存服务器加载器数据并避免一些服务器调用
  • 迁移:简化从 React Router -> Remix SPA -> Remix SSR 的迁移(一旦 Remix 支持 SPA 模式

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

跳过跳跃

BFF 架构中使用 Remix 时,跳过 Remix 服务器跳转并直接访问后端 API 可能会更有优势。这假设您能够相应地处理身份验证并且不受 CORS 问题的影响。您可以按如下方式跳过 Remix BFF 跳转:

  1. 在文档加载时从服务器加载器加载数据
  2. 在所有后续加载时从客户端加载器加载数据

在这种情况下,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. 在文档加载时从服务器加载器加载部分数据
  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

仅依赖于服务器加载器的路由如下所示:

app/routes/server-data-route.tsx
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 <>...</>;
}

仅依赖于客户端加载器的路由如下所示。

app/routes/client-data-route.tsx
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 在客户端中进行
  1. 逐步开始移动 clientLoader -> loader 以开始将数据加载移动到服务器