クライアントデータ
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ホップをスキップできます:
- ドキュメント読み込み時にサーバー
ローダー
からデータを読み込む - その後のすべての読み込みで
クライアントローダー
からデータを読み込む
この場合、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つのデータソースを組み合わせることができます:
- ドキュメント読み込み時にサーバー
ローダー
から部分的なデータを読み込む - SSR中にレンダリングするための
HydrateFallback
コンポーネントをエクスポートする(完全なデータセットがまだないため) clientLoader.hydrate = true
を設定し、これによりRemixに初期ドキュメントハイドレーションの一部としてclientLoaderを呼び出すよう指示する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 <>...</>;}
二者択一
アプリケーション内でデータ読み込み戦略を混在させ、一部のルートではサーバーでのみデータを読み込み、一部のルートではクライアントでのみデータを読み込むことができます。以下のように各ルートで選択できます:
- サーバーデータを使用したい場合は
loader
をエクスポート - クライアントデータを使用したい場合は
clientLoader
とHydrateFallback
をエクスポート
サーバーローダーのみに依存するルートは以下のようになります:
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 <>...</>;}
クライアントローダーのみに依存するルートは以下のようになります:
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 <>...</>;}
クライアントキャッシュ
クライアントキャッシュ(メモリ、ローカルストレージなど)を利用して、一部のサーバー呼び出しをバイパスできます:
- ドキュメント読み込み時にサーバー
loader
からデータを読み込む - キャッシュを準備するために
clientLoader.hydrate = true
を設定する - 後続のナビゲーションで
clientLoader
を通じてキャッシュから読み込む clientAction
内でキャッシュを無効化する
HydrateFallback
コンポーネントをエクスポートしていないため、ルートコンポーネントをSSRし、その後ハイドレーション時にclientLoader
を実行することに注意してください。そのため、ハイドレーションエラーを避けるために、初期読み込み時にloader
とclientLoader
が同じデータを返すことが非常に重要です。
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モードがリリースされた後に個別の移行ガイドを作成する予定ですが、現時点では以下のようなプロセスを想定しています:
createBrowserRouter
/RouterProvider
に移行することで、React Router SPAにデータパターンを導入する- Remix移行の準備を整えるため、SPAをViteを使用するように移行する
- Viteプラグイン(まだ提供されていません)を使用して、ファイルベースのルート定義に段階的に移行する
- React Router SPAをRemix SPAモードに移行し、現在のファイルベースの
loader
関数がすべてclientLoader
として機能するようにする - Remix SPAモード(およびRemix SSRモード)から抜け出し、
loader
関数をclientLoader
に検索/置換する
- この時点でSSRアプリケーションを実行していますが、すべてのデータ読み込みは依然として
clientLoader
を通じてクライアントで行われています
- 段階的に
clientLoader -> loader
の移行を開始し、データ読み込みをサーバーに移動し始める