App RouterにおけるData FetchingとRenderingのバリエーション整理
Next.jsでは、App RouterでServer Component(SC)とClient Component(CC)が区別されるようになってから、Data FetchingとRenderingのバリデーションが増えより複雑になりました。
今回はmicroCMSテンプレートを元に、各Data FetchingとRenderingの挙動を整理します。編集する対象ファイルはapp/articles/[slug]/page.tsx
とします。
このテンプレートはmicroCMSにログインすることで誰でも使用することができるので、よかったら実際に手元で動作確認をしてみてください。
SCでSG
対象のファイルからrevalidate
の記述を削除することで、SCでSGのような挙動を取ります。
export default async function Page({ params, searchParams }: Props) {
const data = await getDetail(params.slug, {
draftKey: searchParams.dk,
});
return <Article data={data} />;
}
App RouterではStatic Renderingがデフォルトなので、fetchはビルド時に一度しか実行されません。
例えば、一度ビルドしたあとにmicroCMSで記事を更新しても、表示は古いままとなります。これはビルド時に取得したデータが返され続けていて、リクエストごとにデータが取得されていないことを意味します。
リクエストごとにデータを取得する(=SSRする)ためには、Dynamic Renderingを有効にする必要があります。
SCでSSR
Dynamic FunctionsまたはUncached Data Requestを使用すると、Dynamic Renderingが有効になります。今回はRoute Segment Configの指定でUncached Data Requestを実行し、Dynamic Renderingを有効にします。
export const fetchCache = 'default-no-store';
export default async function Page({ params, searchParams }: Props) {
const data = await getDetail(params.slug, {
draftKey: searchParams.dk,
});
return <Article data={data} />;
}
これでSSRの挙動となり、リクエストごとにデータを取得するようになります。一度ビルドしたあとにmicroCMSで記事を更新しても、常に最新のデータが表示されます。
ちなみに今回はRoute Segment Configを使用しましたが、microcms-js-sdkでもfetchリクエストオプションが追加できるようになっています。実際には、キャッシュの制御はセグメント単位ではなくリクエスト単位で行いたいと思うので、fetchオプションとしてcache: 'no-store'
を指定するのがおすすめです。
SCでISR
Route Segment Configのrevalidateを使用すると、ISRを実現できます。
export const revalidate = 60;
export default async function Page({ params, searchParams }: Props) {
const data = await getDetail(params.slug, {
draftKey: searchParams.dk,
});
return <Article data={data} />;
}
これで60秒のISRとなります。
SSRと同様に、fetchオプションとしてrevalidateを指定し、リクエストごとにキャッシュを制御することもできます。
fetch('https://...', { next: { revalidate: 60 } })
SCでOn-demand ISR
revalidatePathまたはrevalidateTagを使用することで、On-demand ISRを実現できます。
説明が長くなるので参考記事を添付します。
CCでSSR
対象のファイルをSCからCCに変更するために、以下のように編集します。
'use client';
export default function Page({ params, searchParams }: Props) {
const [data, setData] = useState();
const fetchDetail = async () => {
const res = await getDetail(params.slug, {
draftKey: searchParams.dk,
});
setData(res);
};
useEffect(() => {
fetchDetail();
}, []);
return data ? <Article data={data} /> : <p>loading...</p>;
}
要点は3つ
- ファイルの先頭に
'use client';
ディレクティブを付ける - Pageコンポーネントからasyncを削除する
- fetchは
useEffect
内で実行する
従来のFunctional Componentですね。
Next.jsはデフォルトでSSRの挙動を取るので、loading...
が埋め込まれた状態で、サーバーからクライアントにレスポンスが返されます。
その後、クライアント側でuseEffect
が実行されるので、しばらくloading...
が表示されたあとに<Article />
に切り替わる挙動になります。
実際にクライアント側でfetchする場合には、Tanstack QueryやSWRに代表されるキャッシュ機構を備えたライブラリを使用することになりますし、APIキーが表側に露出しないようにRoute Handlersなどを活用することになると思います。
CCでSSRしない
next/dynamicを使うことで、SSRでは読み込ませず、クライアントでのみ実行するコンポーネントを作成できます。
こちらも説明が長くなるので参考記事を添付します。
Static Exports
詳細は割愛しますが、静的生成も健在です。
Discussion