コンテンツにスキップ

クライアントデータ

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)から来ているが、組み合わされたデータセットを取得するまでコンポーネントをレンダリングできない場合です。以下のように2つのデータソースを組み合わせることができます:

  1. ドキュメント読み込み時にサーバーローダーから部分的なデータを読み込む
  2. SSR中にレンダリングするためのHydrateFallbackコンポーネントをエクスポートする(完全なデータセットがまだないため)
  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>SSR中にレンダリングされるスケルトン</p>; // (2)
}
export default function Component() {
// これは常にサーバー + クライアントデータの組み合わせセットになります
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) - サーバーデータ
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;
}
// 注意:`loader`がない場合、これを明示的に設定する必要はありません - 暗黙的に設定されます
clientLoader.hydrate = true;
// (2)
export function HydrateFallback() {
return <p>SSR中にレンダリングされるスケルトン</p>;
}
export default function Component() {
const data = useLoaderData(); // (2) - クライアントデータ
return <>...</>;
}

クライアントキャッシュ

クライアントキャッシュ(メモリ、ローカルストレージなど)を利用して、一部のサーバー呼び出しをバイパスできます:

  1. ドキュメント読み込み時にサーバーloaderからデータを読み込む
  2. キャッシュを準備するためにclientLoader.hydrate = trueを設定する
  3. 後続のナビゲーションでclientLoaderを通じてキャッシュから読み込む
  4. clientAction内でキャッシュを無効化する

HydrateFallbackコンポーネントをエクスポートしていないため、ルートコンポーネントをSSRし、その後ハイドレーション時に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. Remix移行の準備を整えるため、SPAをViteを使用するように移行する
  3. Viteプラグイン(まだ提供されていません)を使用して、ファイルベースのルート定義に段階的に移行する
  4. React Router SPAをRemix SPAモードに移行し、現在のファイルベースのloader関数がすべてclientLoaderとして機能するようにする
  5. Remix SPAモード(およびRemix SSRモード)から抜け出し、loader関数をclientLoaderに検索/置換する
  • この時点でSSRアプリケーションを実行していますが、すべてのデータ読み込みは依然としてclientLoaderを通じてクライアントで行われています
  1. 段階的にclientLoader -> loaderの移行を開始し、データ読み込みをサーバーに移動し始める