loaderによるデータ取得
ここまでに、Remixには画面の初期化に使うためのデータ取得の処理があり、これが次の3つの用途で利用されることを解説しました。
- サイトの初期表示時のデータ取得(HTMLへ埋め込むためのデータ取得)
- 画面遷移時のページ初期化のためのデータ取得
- フォームの送信後の画面更新のためのデータ取得
これだけ活用される機能となると、複雑なAPIを持っているんじゃないかと心配になりますが、かなりシンプルです。第2回でも簡単に触れましたが、app/routes フォルダ内に配置した、ルート(≒画面)ファイルに、リスト1のように記述します。
import { getUser } from "../lib/db.server"; // (4)
import { useLoaderData } from "@remix-run/react";
/ (1) /
export const loader = async () => {
const user = await getUser();
return { user }; // (2)
};
// この画面に表示するコンポーネント
export default function LoaderSample() {
// (3)
const data = useLoaderData();
return (
<div>
<h1>Loader Sample</h1>
<p>こんにちは、{data.user.name}さん!</p>
</div>
);
}
特別な設定は必要ではなく、(1)のように loader() という名前の関数を設置し、エクスポートすることで有効になります。基本的にasync functionとして記述し、データベースや外部APIから取得したデータを最小限のデータへと加工し、(2)のようにreturnします。loader() が返したデータは、(3)のようにコンポーネント側で useLoaderData() を使うことで取り出すことができます。(2)でreturnしたデータは、一度JSONへとシリアライズされるため、シリアライズできないデータはreturnしないように気をつけてください。
なお、手元で試している方は、(4)のモジュールが必要になりますので、サンプルコードの app/lib/db.server.js からファイルをコピーしてご利用ください。
さて、リスト1を実際に動かして http://localhost:3000/loader-sample にアクセスすると、図7のようになります。
loader() で取得したデータが表示されました。こうして見ていると、loader() が本当にサーバーで動いているのか心配になりますが、ちゃんとサーバーで動いているのでご安心ください。気になる方は、loader() 内で console.log() をどこかに挟んでみてください。ブラウザのコンソールではなく、npm run dev を実行したターミナルのほう(つまりサーバーのログ)にメッセージが表示されます。
ちなみに、loader() はNode.jsやCloudflare Workerのようなサーバーで実行されるものなので、本来の標準ライブラリもサーバーの実行環境に準拠します。しかし、頻出する通信周りは、Web標準に準拠していたほうが学習コストも下がるだろうという判断なのか、Web標準のFetch APIが再現された fetch() 関数が実装されています。そのため、Remixのサーバーサイドでは、随所にFetch APIのオブジェクトが登場します。お楽しみに。
loaderの引数
loader() の引数として、リクエストに関する情報が渡されます。使えるパラメータは request と params です(リスト2)。
export const loader = async ({ request, params }) => { ... };
まずは request について解説します。request はブラウザからのリクエスト内容を表すオブジェクトです。Fetch APIのRequestと同じインターフェースになっていますので、リスト3のような使い方ができます。
export async function loader({ request }) {
// (1)
const cookie = request.headers.get("Cookie");
// (2)
const url = new URL(request.url);
const query = url.searchParams.get("q");
}
request に付与されたヘッダー情報は request.headersで取り出せるので、(1)のようにCookieにアクセスすることもできます。URLに付与されたパラメータはrequest.urlで確認できるので、(2)のように一度URLに読み込ませてから、url.searchParamsで個別のパラメータを参照することも可能です。
次は params について解説します。params はURLのパスにパラメータを設定した場合に利用します。例えば、/users/123 のように、ユーザーIDをパスとして付与するケースを考えます(リスト4)。
export async function loader({ params }) {
// (1)
params.userId; // "123"
}
ルートのファイル名に $userId と命名しているので、URLで /users/ の後に書いた文字列が、パラメータとして扱われます。このとき、(1)のように、params からURLのパス由来のパラメータを取り出すことができます。
これらの情報を組み合わせて、loader() はこのURLで求められている情報を収集し、最終的な戻り値を決定します。
loaderの戻り値
前述の通り、loader() の戻り値は、JSONとしてシリアライズ可能な値であれば何でもOKです。オブジェクトをそのまま返しても、暗黙的にシリアライズしてもらえますが、最も丁寧な書き方はリスト5の書き方です。
export const loader = async () => {
// (1)
return new Response(JSON.stringify({ name: "太郎" }), {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});
};
loader() の戻り値とはレスポンスのことなので、Web標準に則って(1)のFetch APIのResponseで表現できます。JSONへのシリアライズを明示的に JSON.stringify() で行ったり、ヘッダーを application/json に指定したりと、細かい指定を行えます。
ただ、この設定はあまりにも頻出なので、便利関数が用意されています(リスト6)。
import { json } from "@remix-run/node"; // (2)
export const loader = async () => {
return json({ name: "太郎" }); // (1)
};
(1)の json() 関数は、リスト5で Response に設定した内容を実行するエイリアスとなっています。大抵の情報はJSONにシリアライズして application/json としてブラウザに送ることになるので、手癖で付けるようにしておいてもよいでしょう。ちなみに、この json() 関数は(2)のように @remix-run/node パッケージからインポートしています。サンプルで利用しているRemix App ServerはNode.jsなのでこのパッケージを使用していますが、Cloudflare WorkersやDenoといった別のランタイムで動かす場合には、 @remix-run/cloudflare や @remix-run/deno から json() 関数をインポートすることになるので、ご留意ください。
同様の便利関数として、redirect() が用意されています(リスト7)。
import { redirect } from "@remix-run/node";
export const loader = async () => {
const user = await getUser();
if (!user) {
// (1)
return redirect("/login");
// throw redirect("/login") でも可
}
return { user };
};
redirect() は処理を中断して別のパスへリダイレクトするための便利関数です。基本的にはreturnして使いますが、少し特殊な挙動として、throwしても同様の効果があります。認証チェックの共通処理など、loader() 直下ではない(≒returnできない)場所にリダイレクト処理を記載したい場合には、throwで利用するとよいでしょう。
Formとactionによるデータ更新
さて、続いてフォームによるデータ送信について解説します。前述の通り、Remixではデータ送信に原則としてHTMLフォームを利用します。このフォームから送信されたデータは、loader() とペアで用意されているデータ更新用関数である、action() が受け取って処理します。リスト8に、ユーザーの表示名を変更するサンプルを用意しました。
import { json } from "@remix-run/node";
import { createUser, getUser } from "../lib/db.server";
import { Form, useLoaderData } from "@remix-run/react";
export const loader = async () => {
const user = await getUser();
return json({ user });
};
// (1)
export const action = async ({ request }) => {
// (4)
const formData = await request.formData();
const newName = formData.get("name");
// データベースに新しいユーザーを登録する
const user = await createUser(newName);
return json({ ok: true });
};
export default function SubmitSample() {
const data = useLoaderData();
// const actionData = useActionData(); // (2) 必要があれば利用する
return (
<div>
<h1>Submit Sample</h1>
<p>こんにちは、{data.user.name}さん!</p>
{/ (3) /}
<Form method="post">
<label htmlFor="name">名前</label>
<input type="text" id="name" name="name" required />
<button type="submit">Submit</button>
</Form>
</div>
);
}
リスト8を実行すると、図8のようになります。
loader() と同様に、ルートのファイル内に(1)のような action という名前の関数を定義して、エクスポートすることで動作するようになります。action() 関数の引数と戻り値は、loader() と全く同じです。処理の起点がGETかPOSTかの違いがあるだけで、リクエストに対してレスポンスを返すための関数であることは同じだからなのでしょう。もしバリデーション結果の返却などで action() の戻り値をコンポーネントで利用したい場合は、useLoaderData() の代わりに(2)の useActionData() を利用してください。
さて、UI側では(3)のように簡単なフォームを作成しました。フォームを <Form> コンポーネントで作成してあること以外は、HTMLで組まれた普通のフォームです。method="post" なので、submitすると、/submit-sample に対してPOSTリクエストを送信することになります。action() はPOSTリクエストに対するエンドポイントとして機能するので、UI側でsubmitした内容を action() で処理することになるわけです。POSTリクエストを受け取った action() では、(4)のように request.formData() を使用して、リクエストボディであるFormDataを取り出すことができます。
実際に動かしてみましょう。図8の状態から、「Submit」ボタンをクリックしてみます(図9)。
図8では「名無しさん」だった部分が、「なかがわさん」に変わりました。action() によってサーバー内部のデータが更新されただけではなく、loader() が自動で再実行されて、UIが最新のデータで置き換わったのです。特段、非同期通信のハンドリングを行わなくても、データの更新や画面の再描画が実現できるため、開発体験がとても良くなっています。
ちなみに、任意のタイミングでsubmitを行うためのuseSubmit()というフックも用意されています。あまり使う機会はありませんが、FormDataを送信して、action() に処理をさせることができるので、もしsubmitボタンだけでは足りなくなった場合に使ってみてください。
まとめ
Remixのネットワークに関する哲学と、フレームワーク上のデータ授受の仕組みについて解説しました。実際に使ってみると、非同期処理を自分でハンドリングする機会が激減するので、おすすめです。
次回は画面遷移やページの定義についてのAPIを解説します。
