キャッシュで理解するNext.js App Routerのデータ取得
はじめに
Next.js 13.4 から App Router が安定版になりました。
App Routerは今までのPages Routerとは大きく変わっています。
色々変化点はありますが、この記事ではデータ取得方法の変化を解説いたします。
App Routerのデータ取得は、getStaticProps
などが無くなったことを筆頭に今までのインターフェイスから大きく変わっています。App Routerのデータ取得動作はキャッシュの観点から考えると説明ができます。
この記事ではまず、従来の getStaticProps
などを使ったデータ取得と同様の挙動をApp Routerで再現し、キャッシュの観点で動作を解説いたします。次に、App Rotuer になって初めて可能になった細かなデータ取得の制御を紹介します。
検証環境
この記事では、実際の挙動を確認しながら進めていきます。使っているNext.jsのバージョンは 13.4.9
です。
3000
番ポートでNext.jsサーバーを動かしつつ、 3001
番ポートでもNext.jsを動かしてバックエンドAPIを再現しています。
export default function handler(
req: NextApiRequest,
res: NextApiResponse<string>
) {
const now = (new Date()).toISOString();
console.log("Received request: ", now);
res.status(200).json(now);
}
/api/now
にhttpリクエストが送られると、APIはリクエストを受け取った時刻をログに表示しつつ返します。
この記事の中で紹介するサンプルコードはGitHubで公開しています。
従来のデータ取得方法
まずは従来のPages Routerにおけるデータ取得と同じ挙動をApp Routerで再現するコードを紹介していきます。
getStaticProps
Pages Routerでは getStaticProps
を使ってこのようなコードを書くことが可能でした。
import { GetStaticProps, InferGetStaticPropsType } from "next";
export const getStaticProps: GetStaticProps<{
now: string;
}> = async () => {
console.log("getStaticProps");
const res = await fetch("http://localhost:3001/api/now");
const now = await res.json();
return { props: { now } };
};
export default function Page({
now,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<p>time: {now}</p>
</>
);
}
exportされている getStaticProps
関数はビルド時に実行されます。ビルド時に得られたpropsがページリクエストの度に再利用されるので、ページをリロードしても同じ時刻が表示されます。[1]
似たような挙動(全く同じではありません。詳細は後述します。)をするコードをApp Routerで書いてみましょう。
App Routerではこのようになります。
/**
* Pages RouterのgetStaticPropsに相当する
*/
export default async function Page() {
console.log("force-cache");
const res = await fetch("http://localhost:3001/api/now");
const now = await res.json();
return (
<>
<p>time: {now}</p>
</>
);
}
上に貼ったスクリーンキャプチャの通り、ページの更新をしても表示される時刻は同じです。
では、果たしてこの挙動をどう理解すれば良いのでしょうか?
答えはキャッシュの有無にあります。
実はこのようにオプションを付けずに fetch
を呼び出した場合は、 Next.jsでは cache: force-cache
を付けているものとして扱われます。[2]
ビルド時に Page
関数が実行されて、その内部で fetch
が呼ばれ、得られた結果がキャッシュとして保存されます。そして、ページリクエストが来た時はビルド時に作られたキャッシュが再利用されています。そのため、ページをリロードしても常にビルド時の時刻が表示されています。
ここで一つ注意点を紹介しておきます。
実は保存されたキャッシュはビルドを跨いでも有効です。
Pages Routerの getStaticProps
関数はビルドの度に毎回実行されるのに対し、App Routerにおいて cache:force-cache
オプション付きで取得されたデータはビルドを跨いでも有効であるため、ビルド時にデータ取得が行われない可能性がある点には注意が必要です。
それではビルド時に毎回データ取得したい場合はどうすれば良いのでしょうか?
私が調べた限りではビルドを行う前に .next
ディレクトリを削除したら再度データ取得が行われる事は確認できました。ですが、もっと効率的で直感的な方法が欲しいなと思います。もし知っていれば教えて欲しいです。
getServerSideProps
続いて getServerSideProps
を考えてみましょう。
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
export const getServerSideProps: GetServerSideProps<{
now: string;
}> = async () => {
console.log("getServerSideProps");
const res = await fetch("http://localhost:3001/api/now");
const now = await res.json();
return { props: { now } };
};
export default function Page({
now,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<>
<p>time: {now}</p>
</>
);
}
getServerSideProps
を使って書かれた上のコードと同じ挙動をするコードをApp Routerで書くと次のようになります。
/**
* Pages RouterのgetServerSidePropsに相当する
*/
export default async function Page() {
console.log("no-store");
const res = await fetch("http://localhost:3001/api/now", { cache: "no-store" });
const now = await res.json();
return (
<>
<p>time: {now}</p>
</>
);
}
ページを開いてみます。
リロードの度に時刻が変わりました。
Pages Routerでは getServerSideProps
がページリクエストの度に実行され、都度最新の値が props
として Page
関数に渡されています。
App Routerでもページリクエストの度に Page
関数が実行されます。今回は fetch
のオプションで cache:no-store
が指定されているためキャッシュは利用されません。そのため、ページを更新する度に最新の時刻が表示されています。
Incremental Static Regeneration
従来方式の最後として、Incremental Static Regeneration (長いので以降はISRと表記します)を考えてみます。
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
export const getServerSideProps: GetServerSideProps<{
now: string;
}> = async () => {
const res = await fetch("http://localhost:3001/api/now");
const now = await res.json();
return { props: { now }, revalidate: 10 };
};
export default function Page({
now,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<>
<p>time: {now}</p>
</>
);
}
getServerSideProps
関数が返すオブジェクトに revalidate:10
が含まれています。この指定により、Next.js がprops
の値を10秒間保持してくれる、というものでした。
同様の挙動をするApp Routerのコードは次のようになります。
/**
* Pages RouterのIncrementalStaticRegeneration(ISR)に相当する
*/
export default async function Page() {
const res = await fetch("http://localhost:3001/api/now", { next: { revalidate: 10 } });
const now = await res.json();
return (
<>
<p>time: {now}</p>
</>
);
}
今回は fetch
のオプションに cache
では無く revalidate
が与えられています。
ページリクエストの度に Page
関数が実行されますが、その時に前回の実行で得られたデータがまだ有効期限内であれば使い、期限が切れていれば再度データを取得する、といった処理が fetch
関数の内部で行われています。
App Routerではfetchごとの細かいキャッシュ管理が可能に
これまでの部分で、従来Pages Routerのデータ取得インターフェイスをApp Routerに書き換えつつ、キャッシュの観点からApp Routerのデータ取得方式を理解してきました。
Pages Routerではデータの取得タイミングはページ単位で決まります。
ページにおいて gerStaticProps
が定義されていればデータ取得はビルド時に行われ、 getServerSideProps
が定義されていればリクエスト時に行われる、といった挙動になります。
一方 App Routerでは同一ページの中であっても、細かくデータの取得タイミングを制御することが可能になりました。
コード付きで実例を紹介いたします。
有効期限が異なる複数のキャッシュを持つページ
同一ページ内で複数タイミングのデータ取得が発生する事例のコードサンプルを作ってみました。
/**
* 有効期限が異なる複数のキャッシュを持つページ
*/
export default async function Page() {
const nowForceCache = await forceCache();
const nowNoStore = await noStore();
const nowRevalidate = await revalidate();
return (
<>
<p>force-cache: {nowForceCache}</p>
<p>no-store: {nowNoStore}</p>
<p>revalidate: {nowRevalidate}</p>
</>
);
}
const forceCache = async () => {
const res = await fetch("http://localhost:3001/api/now?key=1");
const now = await res.json();
return now;
}
const noStore = async () => {
const res = await fetch("http://localhost:3001/api/now?key=2", { cache: "no-store" });
const now = await res.json();
return now;
}
const revalidate = async () => {
const res = await fetch("http://localhost:3001/api/now?key=3", { next: { revalidate: 10 } });
const now = await res.json();
return now;
}
スクリーンキャプチャを見ていただくとわかるように、 fetch
関数に与えたオプションにより表示される時刻が異なっています。
それぞれの fetch
で異なるキャッシュの有効期限を設定したため、期限に応じてデータの再取得が発生しているため表示される値が変わっていると理解できます。
このように、App Routerではページ単位ではなく、 fetch
単位でデータ取得のタイミングを変えることが可能になりました。
なお、今回のサンプルコードでは、URLのパラメータを使ってそれぞれのfetchから別のURLを呼び出しています。バックエンド側はパラメータによらず同じ挙動(時刻を返す)です。
実は全ての fetch
で同じURLを呼び出すと、 forceCache
を指定してもリクエスト毎に値が変わりました。これはおそらく fetch
の内部でURLをキーとしたキャッシュ管理をしているため、no-storeオプションを付けて fetch
を呼び出した時にURLに対応するキャッシュの値が変わったため、と推測しています。
App Routerのデータ取得はキャッシュで考える
これまで見てきたように、Pages Routerのデータ取得は
- 同一ページの中ではデータ取得タイミングは同じ
- ページがエクスポートする関数によってデータ取得タイミングが決まる
という特徴がありました。
App Routerでは 同一ページの中でも細かくデータ取得タイミングを変えられる という大きな進歩が達成されています。
ここまで見てきたように、App Routerにおけるデータ取得の挙動は キャッシュの有効期限 に注目すると理解しやすいでしょう。
終わりに
Next.js プロジェクトのデータ取得方法はApp Routerになり大きく変わりました。
データの取得タイミングを細かく制御できるようになったのはもちろんメリットですが、それに加えて、これまで使われていた getServerSide
といった Next.js の独自APIが無くなり、 標準APIである fetch
に変わったことで、これから初めて Next.js を学ぶ人にとってわかりやすくなったというメリットも有るのではないかと私は思っています。
この記事を書く前に以下の記事を読み、勉強させていただきました。ありがとうございます。
- Next.js Cacheのアツさをシェアしたい(App Router)
- Next.js 13 の cache 周りを理解する - Automatic fetch() Request Deduping および後続の全3記事
私が書いたこの記事もどなたかの参考になれば幸いです。
-
いわゆるSSGと呼ばれる方式ですが、こちらの記事 を読み、App Router時代においてはこの呼び方はかえって混乱の元になると思うようになりました。そのためこの記事でもSSGという言い方は避けています。 ↩︎
-
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching#static-data-fetching
ただし、リンク先に書かれていますが、コンポーネント内部でcookies
かheaders
が呼ばれた時は、リクエスト情報を使っていると判断して次の節に出てくるno-cache
と同じ動きをします。 ↩︎
Discussion
非常に参考になる資料でした!ありがとうございます。
一点お聞きしたいところがありまして、
getServerSidePropsにrevalidateを指定するのでしょうか?正しくはgetStaticPropsにrevalidateではないでしょうか?
増分静的再生 (ISR)