SHOEISHA iD

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

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

Next.jsの新しい概念を学ぶ

Next.js13で追加されたApp RouterとReact Server Components──UI通信の関係を再定義する

Next.jsの新しい概念を学ぶ 第3回

  • X ポスト
  • このエントリーをはてなブックマークに追加

拡張されたFetch API

 App Router固有の話題として特徴的なのは、サーバー側でもWeb標準に準拠したFetch APIが使えることです。RequestResponseといった、ブラウザでもお馴染みのインターフェースが使えるため、ブラウザ向けにReactコンポーネントを書くときとほとんど同じ感覚で、コンテキストスイッチを最小限にして通信処理を記述できます。

 App RouterのFetch APIは、ただWeb標準を再現しているだけではなく、特にキャッシュ周りの挙動が強化されています。キャッシュをうまく扱うことで、ブラウザからの見かけ上のデータ取得を高速化するチャンスがあるためです。

 まず、Fetch APIのブラウザでの挙動と違う点として、fetch() 関数のオプションである cache の値が、デフォルトで 'force-cache' になっています。明示的に書いた場合はリスト2のようなイメージです。

[リスト2]cacheのデフォルトは'force-cache'になっている
const res = await fetch('https://example.com/api/hoge', {
  cache: 'force-cache',
});
// 以下のコードと同じ
// const res = await fetch('https://example.com/api/hoge');

 リスト2の(つまりデフォルトの)設定で fetch() 関数を実行した場合、まずは必ずキャッシュを検索し、ヒットするものがあれば新しいか古いかに関係なく、そのキャッシュを返します。キャッシュがヒットしなかった場合は、通常の通信を行い、ダウンロードしたリソースでキャッシュを更新します。

 この挙動はデフォルトの挙動としてはやや極端です。一度取得したら、基本的にサーバーを再起動するまでキャッシュが更新されないので、データソースの更新がなかなかブラウザに反映されないことになり、デバッグ中にリロードボタンを連打する罠にハマりがちです。リロードするたびに新しい情報を取得してもよい画面を作っている場合には、リスト3のように cache オプションに 'no-store' を指定します。

[リスト3]キャッシュが不要な場合は'no-store'を指定する
const res = await fetch('https://example.com/api/hoge', {
  cache: 'no-store',
});

 'no-store' を指定することにより、キャッシュを確認せずに常に通信を行うようになります。慣れるまでは、手癖でこの設定を使ってもいいかもしれません。

キャッシュを破棄する方法

 cache オプションはWeb標準にも存在するものでしたが、App Router独自の機能として、キャッシュを破棄するためのAPIが用意されています。'force-cache' の挙動と合わせて使うことを前提として読んでください。

 App Routerでは、キャッシュを破棄して新しいデータを読み込めるようにする方法として、大きく分けて2つのアプローチがあります。

  1. 時間経過による破棄
  2. オンデマンドな破棄

 それぞれ解説していきましょう。まず、時間経過による破棄は、指定した時間の経過後に発生したリクエストでキャッシュを破棄する方法です。fetch() のオプションの独自拡張である next.revalidate を利用します(リスト4)。

[リスト4]時間経過によるキャッシュ破棄
const res = await fetch('https://example.com/api/hoge', {
  next: {
    revalidate: 3600, // (1)
  },
});

 (1) で3600秒(1時間)後を指定したので、キャッシュを作成してから1時間以上が経過すると、次のリクエストでキャッシュが破棄・更新されます。定期的に内容が変わるタイプの、ランキングなどのデータについては、この方法でキャッシュを破棄するとよいでしょう。

 次に、オンデマンドな破棄は、任意のタイミングでキャッシュを破棄する方法です。オンデマンドな破棄を行うための関数は2種類用意されています。

 ひとつは revalidatePath() です。これは引数にページのパスを文字列で取ります。つまり、app/todos/page.js というファイルを作成した場合、revalidatePath('/todos') というように、ページのパスとなる文字列を渡します。この関数を呼び出すと、指定したパスのページ構築に使われている、すべての fetch() でキャッシュが破棄・更新されます。

 もうひとつは revalidateTag() です。これは、fetch() のオプション next.tags と組み合わせて使います。まず、キャッシュの破棄を制御したい fetch()next.tags オプションを追加します。このオプションは、文字列の配列を受け取ります(リスト5)。

[リスト5]タグを使ったキャッシュ破棄
import { revalidateTag } from "next/cache";

const res = await fetch('https://example.com/api/hoge', {
  next: {
    tags: ['hoge', 'fuga'], // (1)
  },
});

// 任意のタイミングでキャッシュを破棄する
revalidateTag('hoge'); // (2)

 (1) では、hogefuga というタグを指定しています。この fetch() で取得したデータは、revalidateTag() によってタグを指定してキャッシュを破棄することができます。revalidateTag() は引数に文字列を取ります。fetch() で指定したタグのうち、いずれかのタグが一致するキャッシュが破棄されます。つまり、(2)のように revalidateTag('hoge') とすると、fetch() で指定した hoge というタグが一致するキャッシュが破棄されます。

 このように、App RouterはWeb標準のFetch APIを活用しつつ、キャッシュの破棄・更新を行うための独自のオプションを提供しています。

古き良きHTML Formを活用するためのServer Actions

 次に、データ送信に関する話題を扱いましょう。React Server Componentsは、データの取得とレンダリングだけではなく、データの送信についてもサポートしています。ブラウザからNext.jsサーバーへのデータ送信については、HTML Form(<form> 要素)を使うことが想定されており、特別な専用のAPIは用意されていません。ただ、少し独特な使い方をするので、少しずつ見ていきましょう。

Server ActionsにFormDataを渡す

 サンプルとしてTodoアプリを作成します。まずは、タスクの表示と追加ができるようにしましょう。図1のような画面を作成します。

図1:Todoアプリの画面
図1:Todoアプリの画面

 ソースコードはリスト6のように作りました。サンプルコードではclassNameにCSSを当てておりますが、記事中のサンプルでは割愛させてください。

[リスト6]app/todos/page.js
import { revalidatePath } from "next/cache";
import { getTodoList, addTodoItem } from "@/app/db";

export default async function TodoList() {
  const todoList = await getTodoList(); // (1)

  // (4)
  async function createTask(formData) {
    "use server"; // (5)
    const title = await formData.get("title"); // (6)
    await addTodoItem(title); // (7)
    revalidatePath("/todos"); // (8)
  }

  return (
    <main>
      <h1>Todo App</h1>
      <h2>新しいタスクを入力する</h2>
      {/* (3) */}
      <form action={createTask}>
        <input type="text" name="title" />
        <button type="submit">追加</button>
      </form>
      {/* (2) */}
      <h2>タスク一覧</h2>
      <div>
        {todoList.map((item) => (
          <div key={item.id} >
            <p>{item.title}</p>
          </div>
        ))}
      </div>
    </main>
  );
}

 まず、タスク一覧で扱うためのデータは(1)のように取得しています。サンプル用の簡単なものですが、通信だとお考えください。取得したデータは(2)で並べています。

 さて、次は(3)のフォームを見ていただきましょう。input要素とsubmitボタンがひとつずつある、素直なHTML Formですが、一箇所だけ奇妙な点があります。action 属性に、createTask という関数を指定しているのです。このように設定することで、submit時のサーバー側でデータハンドリングを行うための関数を定義できます。もちろん、普通のHTMLではこんな書き方はできません。React Server Componentsによる、特殊な前処理によって、ブラウザで描画される時点では普通のHTML Formに変換されます。

 それでは次に、action で指定された(4)の関数を見てみましょう。この関数はReact Server Componentsの一機能でServer Actionsと呼ばれています。まず目につくのは(5)の "use server" という文字列です。これはディレクティブと呼ばれる宣言で、この関数がサーバー側で実行されることを表します。次に、(6)で引数からデータを取り出します。Server Actionsの関数には、フォームから送信された FormData オブジェクトが引数として渡されるので、データを取り出してから、(7)のように別のサーバーなどに送信します。

 最後に、表示中のデータに更新すべき変更があったことをNext.jsへと知らせるために、revalidatePath() を実行しています。この画面は /todos というパスで表示されているので、引数もそれに合わせました。

 これで、フォームにタスク名を入力して送信ボタンを押すと、タスク一覧が更新される、という挙動が実現できました。

Server Actionsにパラメータをバインドする

 さて、action に関数を指定するだけで、サーバー側でFormDataを処理できることはわかりました。では、少し別の例を考えます。図2のような削除ボタンはどう実装すればよいでしょうか。

図2:削除ボタンを押すと、該当のタスクが削除される
図2:削除ボタンを押すと、該当のタスクが削除される

 古き良きHTMLの素直な実装としては、リスト7のように type="hidden" なinput要素を使って、削除対象のタスクIDをフォームに含めて送信するのが一般的です。

[リスト7]app/todos/page.js
// (略)
{todoList.map((item) => (
  <div key={item.id}>
    <p>{item.title}</p>
    <form action={deleteTask}>{/* (2) */}
      {/* (1) */}
      <input type="hidden" name="id" value={item.id} />
      <button>削除</button>
    </form>
  </div>
))}
// (略)

 ただ、(1)のようにhiddenを使う方法だと、タスクIDが(2)で指定した deleteTask() に渡る過程で、一度フォームを通ったことにより、文字列に変換されます。もしタスクIDがnumber型だった場合、numberのままサーバーに届いた方が嬉しいですよね。また、パラメータがDOMツリーに表示されるので、ユーザーに見せたくない情報を隠したいときにはこの方法は使えません。

 そこで、少し特殊な方法を使います。リスト8を見てください。

[リスト8]app/todos/page.js
// (略)
async function deleteTask(id) { // (2)
  "use server";
  await deleteTodoItem(id);
  revalidatePath("/todos");
}
// (略)
{todoList.map((item) => (
  <div key={item.id}>
    <p>{item.title}</p>
    {/* (1) */}
    <form action={deleteTask.bind(null, item.id)}>
      <button>削除</button>
    </form>
  </div>
))}
// (略)

 (1)のように、deleteTask に関数の bind() 機能を使って、タスクIDをバインドしています。このようにすることで、deleteTask() にはタスクIDが引数として渡されます。この方法を使うと、フォームにパラメータを含めることなく、型も変わっていない状態で(2)のようにパラメータを受け取ることができます。

 最終的には、図3のようになります。

図3:Todoアプリ
図3:Todoアプリ

 また、最終的なソースコードはリスト9のようになりました。

[リスト9]app/todos/page.js
import { revalidatePath } from "next/cache";
import { getTodoList, addTodoItem, deleteTodoItem } from "@/app/db";

export default async function TodoList() {
  const todoList = await getTodoList();

  async function createTask(formData) {
    "use server";
    const title = await formData.get("title");
    await addTodoItem(title);
    revalidatePath("/todos");
  }

  async function deleteTask(id) {
    "use server";
    await deleteTodoItem(id);
    revalidatePath("/todos");
  }

  return (
    <main>
      <h1>Todo App</h1>
      <h2>新しいタスクを入力する</h2>
      <form action={createTask}>
        <input type="text" name="title" />
        <button type="submit">追加</button>
      </form>
      <h2>タスク一覧</h2>
      <div>
        {todoList.map((item) => (
          <div key={item.id}>
            <p>{item.title}</p>
            <form action={deleteTask.bind(null, item.id)}>
              <button>削除</button>
            </form>
          </div>
        ))}
      </div>
    </main>
  );
}

まとめ

 3回にわたって、App Routerの独特の世界観について触れてきました。初めは見慣れない記法に目が泳ぎますが、慣れてくると直感的に通信とUIを統合できる、良いフレームワークに見えてきます。ぜひ、実際に手を動かしてみてください。

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

  • X ポスト
  • このエントリーをはてなブックマークに追加
Next.jsの新しい概念を学ぶ連載記事一覧

もっと読む

この記事の著者

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

WINGSプロジェクトについて> 有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS Twitter: @yyamada(公式)、@yyamada/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本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

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

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

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/18980 2024/02/21 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング