データ授受に関する設定
前回に続いて、ルートモジュールに設定できるデータ授受に関する設定について解説します。この区分に分類できるのは、表1の5つです。
| 名称 | 概要 |
|---|---|
| loader | データフェッチのためのサーバー側の関数 |
| action | フォームからのsubmitリクエストを受け付けるサーバー側の関数 |
| clientLoader | データフェッチのためのクライアント側の関数 |
| clientAction | フォームからのsubmitリクエストを受け付けるクライアント側の関数 |
| shouldRevalidate | どのようなイベントが発生したときにloaderを再実行するのかを定義する |
数は多いですが、基本的には、サーバー側で実行する loader と action と、ブラウザ側で実行する clientLoader と clientAction という対になる関数があり、それぞれの使い方も似ているので、さほど覚えることは多くないはずです。それでは、見ていきましょう。
loader / clientLoader
まず解説するのは、Remixの最大の特徴の1つともいえる、loader です。これは、サーバー側で実行される関数で、データフェッチのために使います。もう1つ、ブラウザ側のデータを loader と同じスキームで取得するために用意された、clientLoader についても合わせて解説しましょう。リスト1のように定義します。
import { json } from '@remix-run/node';
export async function loader({ params }) {
const { itemId } = params;
const item = await fetch(`https://example.com/item/${itemId}`)
.then((res) => res.json());
// (1)
return json({
item,
});
}
export async function clientLoader({ serverLoader }) {
const serverData = await serverLoader(); // (3)
const settings = getSettings(); // (4)
// (5)
return {
...serverData,
settings,
};
}
export default function Component() {
const { item, settings } = useLoaderData(); // (6)
// 省略
}
loader については本連載の中で何度か例示してきたように、サーバー側でデータフェッチを行い、その結果のみをブラウザへと返すものです。戻り値としては、原則としてWeb標準の Response 型のデータを返すことになっていますが、JSONとして表現可能なオブジェクトを返す場合には、(1)のように json() 関数を使って、Content-Type: application/json を設定済みの Response 型のデータを返すこともできます。
本連載の中では少し見慣れないのが、(2)の clientLoader です。これはその名の通り、ブラウザ側で実行されるloaderです。特徴としては、(3)のように、引数で受け取った serverLoader 関数で loader 関数の結果を受け取りつつ、(4)のタイミングでブラウザ独自の処理を行った上で、(5)のようにコンポーネントに対してデータを渡すことができます。 ブラウザのAPIからデータを取り出す際には、clientLoader を利用するのがオススメです。また、処理時間が長そうな場合には、前回解説した HydrateFallback を併用するとよいでしょう。
最終的には、(6)のように、useLoaderData フックを使って、clientLoader で返したデータを取り出します。loader と clientLoader のどちらを使っているのかは、コンポーネント側はあまり意識する必要はありません。
さて、loader や clientLoader が引数でオブジェクトとして受け取れるデータについても確認しておきましょう。受け取れるデータは、次の通りです。
-
params:URLパスのパラメーター -
request:HTTPリクエスト -
context:(loaderのみ)Remixのコンテキスト -
serverLoader:(clientLoaderのみ)loaderが返したデータを取得するための関数
params は、mypage.$itemId.jsx のようにファイル名にパラメータを持つルートモジュールの場合に、loader や clientLoader の中で params.itemId としてURL内のパラメータを呼び出すことができるプロパティです。request は、Web標準のRequest型のデータで、HTTPリクエストの情報を取得するために使います。request.url でページのURL全体が取れるので、Search Params(いわゆるクエリパラメータ)を読み出すことも可能です。context は、Remixのコンテキストを取得するために使います。Node.js環境で動かしていると使う機会が少ないのですが、Cloudflare Workers等の環境では、AIやデータベースといった組み込みサービスを呼び出すために使用されることもあります(リスト2)。
export const loader = ({ context }) => { // contextからCloudflare WorkersのAPIにアクセスできる const { env } = context.cloudflare; const answer = await env.AI.run('@cf/meta/llama-2-7b-chat-int8', { prompt: "What is the origin of the phrase 'Hello, World'" }); // 略 };
serverLoader は、clientLoader でのみ利用可能なプロパティで、loader が返したデータを取得するために使います。
action / clientAction
次は、action と clientAction です。これは、フォームからのsubmitリクエストを受け付けるための関数です。action はサーバー側で実行される関数で、clientAction はブラウザ側で実行される関数です。リスト3のように定義します。
import { json } from '@remix-run/node';
import { useLoaderData, useActionData, Form } from '@remix-run/react';
export async function loader({ params }) {
return json({ itemId: params.itemId });
}
export async function action({ params, request }) {
// (1)
const { itemId } = params;
// (3)
const formData = await request.formData();
const title = await formData.get('title');
// (5) バリデーションを行う
if (!title) {
return json({
success: false,
message: 'Title is required',
});
}
await fetch(`/api/items/${itemId}`, {
method: 'POST',
body: JSON.stringify({ title }),
});
return json({ success: true });
}
export async function clientAction({ serverAction }) {
/* サーバーへデータを送信する前に、ブラウザ側でやっておきたいことがあればここで実施する */
// (4)
const result = await serverAction();
return {
...result,
// ここで何かしらの値を追加することもできる
};
}
export default function ActionComponent() {
const { itemId } = useLoaderData();
const actionData = useActionData();
console.log('itemId', itemId);
console.log('actionData', actionData);
return (
<div>
<h1>Action</h1>
{actionData?.success === false && ( // (6)
<p style={{ color: 'red' }}>
{actionData.message}
</p>
)}
<Form method="post">
<input type="text" name="title" />{/* (2) */}
<button type="submit">Submit</button>
</Form>
</div>
);
}
action は loader と同じインターフェースを持つため、(1)のように params からルーティングに定義したパラメータ($itemId)を受け取ることができます。また、(2)のようなフォームからのリクエストを受け付ける action ならではの特性として、(3)のように request からFormData型のデータを取り出すことができます。
clientAction はブラウザ側で実行される関数で、ほとんど action と同じインターフェースを持ちます。(4)の serverAction() 関数をパラメータから受け取れる点が action との主な違いです。serverAction() 関数を実行すると、サーバー側の action が実行されます。この点も clientLoader と類似していますね。なお、clientAction はあまり使い所が多くありません。ブラウザ内に独自にキャッシュを構築している場合に、そのキャッシュを更新するために使う方法が公式ドキュメントで示唆されていますが、それ以外の場合には、action で十分です。
useActionData() フックを使うことで、action や clientAction で返したデータを取得することができます。使いどころとしては、(5)のようにサーバー側でバリデーションした結果をブラウザに返すために使うことが多いでしょう。バリデーションの結果を使って、(6)のように、エラーメッセージを表示するような使い方が有効です。(5)のチェックによって title が空の場合にエラーメッセージを発行するので、リスト3のコードを実行して、フォームに何も入力せずに送信すると、図1のようにエラーメッセージが表示されます。
action でバリデーションを行うことで、検証内容の詳細な処理をブラウザに対して隠せるメリットがあります。フォームの送信時には、action と useActionData を活用することをおすすめします。
shouldRevalidate
データ授受の最後に、shouldRevalidate について解説します。これは、画面遷移やデータ送信のイベントごとにブラウザ側で実行して、loader を再実行するかどうかを判定するための関数です。リスト4のように定義します。
export const shouldRevalidate = ({
actionResult,
currentParams,
currentUrl,
defaultShouldRevalidate,
formAction,
formData,
formEncType,
formMethod,
nextParams,
nextUrl,
}) => {
// (1) loaderを再実行するならtrueを返す
if (nextParams.forceReload === true) { // (3)
return true;
}
// (2) loaderを再実行しないならfalseを返す
if (/* loaderを再実行しない条件 */) {
return false;
}
return defaultShouldRevalidate; // 特に条件を指定しない場合はこちらを使う
};
(1)のように、最終的に true を返せば再実行する、(2)のように false を返せば再実行しない、という単純な仕組みです。各種イベントの内容を検知するために、引数からは次のような値が受け取れます。
-
フォームからのデータ送信
-
formAction:リクエスト時の
action属性の値 -
formData:リクエスト時の
FormData型のデータ - formEncType:リクエスト時のデータのエンコーディング
-
formMethod:リクエスト時の
method属性の値 -
actionResult:
actionやclientAction関数の戻り値
-
formAction:リクエスト時の
-
画面遷移
- currentParams:現在のURLパスのパラメーター
- currentUrl:現在のURL
- nextParams:次のURLパスのパラメーター
- nextUrl:次のURL
-
その他
- defaultShouldRevalidate:デフォルトの判定結果
基本的には、関心のある値の変化のみを判定して、それ以外のケースでは defaultShouldRevalidate を返すのがベストプラクティス
のようです。defaultShouldRevalidate には、loader が実行されうるタイミングごとに最適な値が渡されます。
たとえば、shouldRevalidate() をデフォルト設定のままにして、/users/123/article/abc から /users/123/article/def へ画面遷移した場合について考えてみましょう。パスだけで見れば app/routes/users.$user_id.jsx と app/routes/users.$user_id.article.$article_id.jsx の loader が実行されそうです。しかし、実際には、後者の loader だけが実行されます。これは、画面遷移の前後で /users/123 というパスが変わっていないため、app/routes/users.$user_id.jsx の loader を再実行する必要がないとRemixが判断して、defaultShouldRevalidate に false を渡したからです。このように、特段の理由がない限りは defaultShouldRevalidate の挙動に任せられるように shouldRevalidate() を実装しておくとよいでしょう。
特定の操作による画面遷移のときだけ loader を再実行したい場合には、app/routes/users.$user_id.jsx の shouldRevalidate へ、(3)のような設定を。行うとよいでしょう。forceReload というパラメータをURLに付与して、そのパラメータがある場合には true を返すようにしておくと、特定の操作による画面遷移のときだけ loader を再実行することができます。つまり、/users/123/article/abc から /users/123/article/def?forceReload=true への画面遷移した際に、app/routes/users.$user_id.jsx の loader を再実行することができます。
まとめ
今回は、ルートモジュールに設定できる各種の設定のうち、データ授受に関する設定について解説しました。loader や action といった基本的な設定から、clientLoader や clientAction といったブラウザ側で実行される設定、shouldRevalidate といった loader の再実行を制御する設定まで、幅広い設定項目を見てきました。Webアプリケーションにとって通信は欠かせない要素ですが、Remixを使えば、それらの設定を簡単に行うことができます。
これらのAPIを活用した、Remixと相性のよいフォーム管理やバリデーションの手法もあるので、本連載の後の回に解説できればと思います。
次回はルーティング設定の最後のトピックとして、HTTPヘッダーやmeta要素の設定など、ルートモジュールに設定できるその他の設定について解説します。お楽しみに!
