クライアントデータ

Remixはv2.4.0で「クライアントデータ」(RFC)のサポートを導入しました。これにより、ルートからclientLoader/clientActionをエクスポートすることで、ブラウザでルートローダー/アクションを実行することを選択できます。

これらの新しいエクスポートは少し扱いが難しいものであり、主要なデータ読み込み/送信メカニズムとして推奨されるものではありません。代わりに、以下のような高度なユースケースに対応するためのレバーとして利用できます。

  • ホップをスキップ: ブラウザから直接データAPIをクエリし、ローダーをSSRのためだけに使用する
  • フルスタック状態: ローダーデータの完全なセットのために、サーバーデータをクライアントデータで補強する
  • どちらか一方: サーバーローダーを使用する場合もあれば、クライアントローダーを使用する場合もあるが、1つのルートで両方を使用することはない
  • クライアントキャッシュ: サーバーローダーデータをクライアントにキャッシュし、一部のサーバー呼び出しを回避する
  • 移行: 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)から取得されるものの、データの組み合わせが揃うまでコンポーネントをレンダリングできない場合です。これらの2つのデータソースを組み合わせる方法は次のとおりです。

  1. ドキュメントのロード時に、サーバーのloaderから部分的なデータをロードします。
  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へのマイグレーションに備えて、Viteを使用するようにSPAを移行する
  3. Viteプラグイン(まだ提供されていません)を使用して、ファイルベースのルート定義に段階的に移行する
  4. React Router SPAをRemix SPAモードに移行する。この際、現在のファイルベースのloader関数はすべてclientLoaderとして機能する
  5. Remix SPAモードをオプトアウト(Remix SSRモードに移行)し、loader関数をclientLoaderに検索/置換する
    • これでSSRアプリが実行されますが、すべてのデータローディングはclientLoaderを介してクライアントで実行されます
  6. clientLoader -> loaderへの移行を段階的に開始し、データローディングをサーバーに移行し始める