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