¥Client Data
Remix 在 v2.4.0
中引入了对 "客户端数据" (RFC) 的支持,允许你选择通过路由中的 clientLoader
/clientAction
导出在浏览器中运行路由加载器/操作。
¥Remix introduced support for "Client Data" (RFC) in v2.4.0
which allows you to opt-into running route loaders/actions in the browser via clientLoader
/clientAction
exports from your route.
这些新的导出功能有点棘手,不建议将其作为主要的数据加载/提交机制 - 但它们会为你提供以下一些高级用例的辅助工具:
¥These new exports are a bit of a sharp knife and are not recommended as your primary data loading/submission mechanisms — but instead give you a lever to pull on for some of the following advanced use cases:
跳过跳跃:直接从浏览器查询数据 API,仅使用加载器进行服务端渲染 (SSR)
¥Skip the Hop: Query a data API directly from the browser, using loaders simply for SSR
全栈状态:使用客户端数据扩充服务器数据,以获得完整的加载器数据。
¥Fullstack State: Augment server data with client data for your full set of loader data
二选一:有时你会使用服务器加载器,有时你会使用客户端加载器,但不会在同一条路由上同时使用这两种加载器。
¥One or the Other: Sometimes you use server loaders, sometimes you use client loaders, but not both on one route
客户端缓存:在客户端缓存服务器加载器数据,避免部分服务器调用
¥Client Cache: Cache server loader data in the client and avoid some server calls
迁移:简化从 React Router → Remix SPA → Remix SSR 的迁移(一旦 Remix 支持 SPA 模式)
¥Migration: Ease your migration from React Router → Remix SPA → Remix SSR (once Remix supports SPA Mode)
请谨慎使用这些新的导出!如果你不小心,你的 UI 很容易不同步。Remix 开箱即用,尽力确保不会发生这种情况。 - 但是,一旦你控制了自己的客户端缓存,就有可能阻止 Remix 执行其正常的服务器 fetch
调用。 - 那么 Remix 无法再保证你的 UI 保持同步。
¥Please use these new exports with caution! If you're not careful — it's straightforward to get your UI out of sync. Remix out of the box tries very hard to ensure that this doesn't happen - but once you take control over your own client-side cache, and potentially prevent Remix from performing its normal server fetch
calls - then Remix can no longer guarantee your UI remains in sync.
¥Skip the Hop
在 BFF 架构中使用 Remix 时,跳过 Remix 服务器跳转并直接访问后端 API 可能会更有利。这假设你能够相应地处理身份验证,并且不受 CORS 问题的影响。你可以按如下方式跳过 Remix BFF 跳转:
¥When using Remix in a BFF architecture, it may be advantageous to skip the Remix server hop and hit your backend API directly. This assumes you are able to handle authentication accordingly and are not subject to CORS issues. You can skip the Remix BFF hop as follows:
在文档加载时从服务器 loader
加载数据
¥Load the data from server loader
on the document load
在所有后续加载时从 clientLoader
加载数据
¥Load the data from the clientLoader
on all subsequent loads
在这种情况下,Remix 不会在 hydration 时调用 clientLoader
。 - 并且只会在后续导航中调用它。
¥In this scenario, Remix will not call the clientLoader
on hydration - and will only call it on subsequent navigations.
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;
}
¥Fullstack State
有时,你可能希望利用 "全栈状态",其中部分数据来自服务器,部分数据来自浏览器(例如 IndexedDB
或其他浏览器 SDK)。 - 但是,在获得合并的数据集之前,你无法渲染组件。你可以按如下方式组合这两个数据源:
¥Sometimes, you may want to leverage "Fullstack State" where some of your data comes from the server, and some of your data comes from the browser (i.e., IndexedDB
or other browser SDKs) - but you can't render your component until you have the combined set of data. You can combine these two data sources as follows:
在文档加载时从服务器 loader
加载部分数据
¥Load the partial data from server loader
on the document load
由于我们尚未获得完整的数据集,因此请导出 HydrateFallback
组件以便在 SSR 期间进行渲染
¥Export a HydrateFallback
component to render during SSR because we don't yet have a full set of data
设置 clientLoader.hydrate = true
,这将指示 Remix 在初始文档处理过程中调用 clientLoader
¥Set clientLoader.hydrate = true
, this instructs Remix to call the clientLoader as part of initial document hydration
在 clientLoader
中将服务器数据与客户端数据相结合
¥Combine the server data with the client data in 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 and client data
const data = useLoaderData();
return <>...</>;
}
¥One or the Other
你可能需要在应用中混合搭配数据加载策略,以便某些路由仅在服务器上加载数据,而某些路由仅在客户端加载数据。你可以按如下方式为每个路由进行选择:
¥You may want to mix and match data loading strategies in your application such that some routes only load data on the server and some routes only load data on the client. You can choose per route as follows:
当你想使用服务器数据时,请导出 loader
¥Export a loader
when you want to use server data
当你想使用客户端数据时,请导出 clientLoader
和 HydrateFallback
¥Export clientLoader
and a HydrateFallback
when you want to use client data
仅依赖于服务器加载器的路由如下所示:
¥A route that only depends on a server loader looks like this:
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 <>...</>;
}
仅依赖于客户端加载器的路由如下所示。
¥A route that only depends on a client loader looks like this.
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 <>...</>;
}
¥Client Cache
你可以利用客户端缓存(内存、本地存储等)绕过某些对服务器的调用,如下所示:
¥You can leverage a client-side cache (memory, local storage, etc.) to bypass certain calls to the server as follows:
在文档加载时从服务器 loader
加载数据
¥Load the data from server loader
on the document load
设置 clientLoader.hydrate = true
以准备缓存
¥Set clientLoader.hydrate = true
to prime the cache
通过 clientLoader
从缓存加载后续导航
¥Load subsequent navigations from the cache via clientLoader
使 clientAction
中的缓存无效
¥Invalidate the cache in your clientAction
请注意,由于我们没有导出 HydrateFallback
组件,我们将对路由组件进行服务器端渲染 (SSR),然后在 Hyption 运行 clientLoader
,因此务必确保 loader
和 clientLoader
在初始加载时返回相同的数据,以避免 Hyption 错误。
¥Note that since we are not exporting a HydrateFallback
component, we will SSR the route component and then run the clientLoader
on hydration, so it's important that your loader
and clientLoader
return the same data on initial load to avoid hydration errors.
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;
}
¥Migration
我们预计在 SPA 模式 发布后会编写一份单独的迁移指南,但目前我们预计该流程将类似于:
¥We expect to write up a separate guide for migrations once SPA Mode lands, but for now we expect that the process will be something like:
通过迁移到 createBrowserRouter
/RouterProvider
,在你的 React Router SPA 中引入数据模式
¥Introduce data patterns in your React Router SPA by moving to createBrowserRouter
/RouterProvider
将你的 SPA 迁移到 Vite,以便更好地为 Remix 迁移做准备
¥Move your SPA to use Vite to better prepare for the Remix migration
通过使用 Vite 插件(尚未提供)逐步迁移到基于文件的路由定义
¥Incrementally move to file-based route definitions via the use of a Vite plugin (not yet provided)
将你的 React Router SPA 迁移到 Remix SPA 模式,其中所有当前基于文件的 loader
函数都充当 clientLoader
¥Migrate your React Router SPA to Remix SPA Mode where all current file-based loader
function act as clientLoader
退出 Remix SPA 模式(进入 Remix SSR 模式),并查找/替换 loader
函数到 clientLoader
¥Opt out of Remix SPA Mode (and into Remix SSR mode) and find/replace your loader
functions to clientLoader
你现在正在运行一个服务器端渲染应用,但所有数据加载仍然通过 clientLoader
在客户端进行。
¥You're now running an SSR app, but all your data loading is still happening in the client via clientLoader
逐步开始迁移 clientLoader -> loader
,以将数据加载迁移到服务器
¥Incrementally start moving clientLoader -> loader
to start moving data loading to the server