SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine(コードジン) DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Remixを通じてWebを学ぶ

Remixで古き良き通信体験を取り戻そう──データ取得とデータ更新について解説

Remixを通じてWebを学ぶ 第4回

loaderによるデータ取得

 ここまでに、Remixには画面の初期化に使うためのデータ取得の処理があり、これが次の3つの用途で利用されることを解説しました。

  • サイトの初期表示時のデータ取得(HTMLへ埋め込むためのデータ取得)
  • 画面遷移時のページ初期化のためのデータ取得
  • フォームの送信後の画面更新のためのデータ取得

 これだけ活用される機能となると、複雑なAPIを持っているんじゃないかと心配になりますが、かなりシンプルです。第2回でも簡単に触れましたが、app/routes フォルダ内に配置した、ルート(≒画面)ファイルに、リスト1のように記述します。

[リスト1]app/routes/loader-sample.jsx
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のようになります。

図7:loaderから取得したデータを表示する
図7:loaderから取得したデータを表示する

 loader() で取得したデータが表示されました。こうして見ていると、loader() が本当にサーバーで動いているのか心配になりますが、ちゃんとサーバーで動いているのでご安心ください。気になる方は、loader() 内で console.log() をどこかに挟んでみてください。ブラウザのコンソールではなく、npm run dev を実行したターミナルのほう(つまりサーバーのログ)にメッセージが表示されます。

 ちなみに、loader() はNode.jsやCloudflare Workerのようなサーバーで実行されるものなので、本来の標準ライブラリもサーバーの実行環境に準拠します。しかし、頻出する通信周りは、Web標準に準拠していたほうが学習コストも下がるだろうという判断なのか、Web標準のFetch APIが再現された fetch() 関数が実装されています。そのため、Remixのサーバーサイドでは、随所にFetch APIのオブジェクトが登場します。お楽しみに。

loaderの引数

 loader() の引数として、リクエストに関する情報が渡されます。使えるパラメータは requestparams です(リスト2)。

[リスト2]loaderの引数
export const loader = async ({ request, params }) => { ... };

 まずは request について解説します。request はブラウザからのリクエスト内容を表すオブジェクトです。Fetch APIのRequestと同じインターフェースになっていますので、リスト3のような使い方ができます。

[リスト3]loaderでrequestを利用する
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)。

[リスト4]app/routes/users/$userId.jsx
export async function loader({ params }) {
  // (1)
  params.userId; // "123"
}

 ルートのファイル名に $userId と命名しているので、URLで /users/ の後に書いた文字列が、パラメータとして扱われます。このとき、(1)のように、params からURLのパス由来のパラメータを取り出すことができます。

 これらの情報を組み合わせて、loader() はこのURLで求められている情報を収集し、最終的な戻り値を決定します。

loaderの戻り値

 前述の通り、loader() の戻り値は、JSONとしてシリアライズ可能な値であれば何でもOKです。オブジェクトをそのまま返しても、暗黙的にシリアライズしてもらえますが、最も丁寧な書き方はリスト5の書き方です。

[リスト5]Web標準のResponseを使用してレスポンス情報を構築する
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)。

[リスト6]JSONデータを返却する場合の便利関数
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)。

[リスト7]redirectでデータ取得処理を中断してリダイレクトできる
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に、ユーザーの表示名を変更するサンプルを用意しました。

[リスト8]app/routes/submit-sample.jsx
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のようになります。

図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)。

図9:フォームを送信してみた
図9:フォームを送信してみた

 図8では「名無しさん」だった部分が、「なかがわさん」に変わりました。action() によってサーバー内部のデータが更新されただけではなく、loader() が自動で再実行されて、UIが最新のデータで置き換わったのです。特段、非同期通信のハンドリングを行わなくても、データの更新や画面の再描画が実現できるため、開発体験がとても良くなっています。

 ちなみに、任意のタイミングでsubmitを行うためのuseSubmit()というフックも用意されています。あまり使う機会はありませんが、FormDataを送信して、action() に処理をさせることができるので、もしsubmitボタンだけでは足りなくなった場合に使ってみてください。

まとめ

 Remixのネットワークに関する哲学と、フレームワーク上のデータ授受の仕組みについて解説しました。実際に使ってみると、非同期処理を自分でハンドリングする機会が激減するので、おすすめです。

 次回は画面遷移やページの定義についてのAPIを解説します。

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
Remixを通じてWebを学ぶ連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 中川 幸哉(ナカガワ ユキヤ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。 2026年時点での登録メンバは約50名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS X: @WingsPro_info(公式)、@WingsPro_info/wings(メンバーリスト) Facebook

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/18232 2023/09/06 11:00

おすすめ

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー