Reactのさまざまなデータフェッチ方法を比較して理解して正しく使用する - SSR + App Router Cache編
「Reactのさまざまなデータフェッチ方法を比較して理解して正しく使用する」シリーズの3記事目、最終記事です🌟
今回は「 Next.js Pages Router(SSR)でのデータフェッチとApp Routerでのデータフェッチ」について理解を深めていきます。
また、最後の全体の結果で「Reactのさまざまなデータフェッチ」シリーズの総括をしていきます。
- イントロ+useEffectを用いたデータフェッチ
- SWR・TanStack Queryを用いたデータフェッチ
- Pages Router(SSR)でのデータフェッチ+App Routerでのデータフェッチ+まとめ ← 👀この記事
Repository
以下は今シリーズで用いたリポジトリです。
🔽クライアントサイドフェッチの調査に用いたリポジトリ:React+Vite(useEffect, SWR・TanStack Query) 🔽サーバーサイドフェッチの調査に用いたリポジトリ:Next.js Pages Router, App Router
Pages Router(SSR)でのデータフェッチ
Next.jsのPages Routerでは標準で SSR (Server Side Rendering) 機能が提供されており、上手く活用することでパフォーマンス・SEOの両面で恩恵を受けられます。
Pre-rendering and Data Fetching
Rendering

SSRとCSRの比較 (Learn Next.js - Pre-rendering and Data Fetching より引用)
もちろん、Next.jsではSSRをしつつも、useEffectなどを使用してデータフェッチをクライアントサイドに寄せることができます。
しかし、サーバサイドでデータフェッチを行うことで、SEO対策が施しやすかったり、レイアウトシフトの軽減など、パフォーマンス面で恩恵を受けられる可能性があります。
これらは、サーバサイドでデータ取得まで行い、初期データが注入されたHTML(+HydrationのためのJS)をブラウザに返却するSSRで実現することができます。
Pages Router(SSR)でのデータフェッチの調査
そんなSSRが可能なNext.jsのPages Router環境でデータフェッチの調査をしていきます💫
Next.jsでは、SSR時の初期データの注入はgetServerSidePropsという非同期の関数をexportすることによって実現できます。
今回のサンプルでは、getServerSideProps の中で直接レンダリング時に必要となるデータを取得しています。
取得したデータをpropsプロパティを持つオブジェクトとしてreturnすることで、SSR時にそのデータがpropsとしてJSX(TSX)に注入されます。
API Routes との使い分け
ところで、getServerSideProps内でNext.jsのAPI Routesで定義したAPIを使わず、getRandomNumber() や getUer()といったAPIの内部処理を直接使用したのはなぜだったのでしょうか?😶
もしgetServerSidePropsにfetch('${process.env.BASE_URL}/route/to/api')などを渡してしまうと、サーバ上でgetServerSidePropsに加えてAPIそのものが実行されるAPI Routesのどちらも実行され、余計なリクエストが発生してしまうからです。
It can be tempting to reach for an API Route when you want to fetch data from the server, then call that API route from getServerSideProps. This is an unnecessary and inefficient approach, as it will cause an extra request to be made due to both getServerSideProps and API Routes running on the server.
先ほどの例で考えてみると、getRandomNumber() や getUer() と同等の処理内容を返す API Routes が存在していたとしても、それをgetServerSidePropsからfetch()などで呼び出すことは避け、内部ロジックのみを流用して直接実行することが推奨されます。
次の例のようにgetServerSidePropsから API Route で定義したAPIにリクエストを送ると、二重でリクエストが発生します。特別な理由がない限りはこのような書き方は避けたほうが望ましいです。
🔽 次のコードのように、getServerSideProps内でAPI Routeで定義したAPIにリクエストを送ると二重にリクエストを送ることになる
const fetcher = (url: string) =>
fetch(url, {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
}).then((res) => res.json())
export async function getServerSideProps() {
console.time("ssr");
const props = Promise.all([
fetcher("https://api.github.com/repos/vercel/next.js"),
fetcher(`${process.env.BASE_URL}/api/get/unstable/data`),
fetcher(`${process.env.BASE_URL}/api/get/user`),
])
.then(([data, randomNumber, user]) => {
console.timeEnd("ssr");
return { props: { data, randomNumber, user } };
})
.catch((err) => {
log(err);
});
return props;
}
getServerSidePropsでサーバサイドのデータフェッチの仕組みを完成させたところで、実際にその様子をのぞいてみましょう👀
データ取得がどこで行われているかを確認するためにgetServerSidePropsの中にconsole.time(); console.timeEnd();を仕込みました。
サーバターミナルにデータ取得にかかった秒数が表示されるのか、ブラウザのコンソールタブに表示されるのかをみてみます。
SSRのページをリロードしてデータを再取得してみます。

ブラウザコンソールの表示
ブラウザコンソールには何も表示されていないようです。
localhostのターミナルはどうでしょうか?

ターミナルの表示
こちらにデータ取得にxxx msかかったとログが出ていました!
きちんとサーバサイドフェッチできてますね👏🏻
このように、getServerSidePropsを使用するとデータ取得の処理をサーバ側で行うことができ、SSR時のレンダリング結果に含めることができます。
また、ネットワークスロットリングをしても、サーバ側でデータ取得の処理をしているのでその影響を受けません。(レンダリング後のHTMLをDLする時はその限りではありません)
それでは、Personコンポーネントでユーザ名を更新してみましょう🤾🏻♀️
bodyにformからのデータを付与したPOSTリクエストを/api/update/userに送ると、DBの値が更新されます。通常通りです。
更新したデータをUIに反映します。
ここではNext.jsのPages Routerを使用しているのでnext/routerからエクスポートされているuseRouterの機能router.reload();を利用して再レンダリングをトリガーしました。
結果
getServerSidePropsを用いた時のデータ取得・更新の挙動です。

SSR時にデータを取得しているので、データが注入された状態のHTMLのみが送られてくる
getServerSidePropsの特徴
getServerSidePropsの利用が許可されているのはpageからのみで、それぞれの子コンポーネントがgetServerSidePropsをデータフェッチのために独立して使うということはないです。
When does getServerSideProps run
getServerSideProps can only be exported from a page. You can’t export it from non-page files.
したがって、pageのgetServerSidePropsで取得されたデータをpropsでバケツリレー式に子コンポーネントに渡していく形となります。(つまり、コンポーネントとデータの依存関係を剥がすことは難しそうです。)
(余談)getServerSidePropsのリクエストのキャッシュ
pageで使用されているgetServerSidePropsのリクエストのキャッシュは本番環境でのみ、以下の設定を加えることで可能なようです。
(※今回は開発環境での調査のみ行なっているため、この機能は利用していません)
App Routerでのデータフェッチ
最後に、Next.js App Routerのキャッシュ機構を用いたデータフェッチと再検証を見ていきます。
App Routerでのデータフェッチの調査
RSC(React Server Components)をNext.js App Router環境で使用します。
(page.tsx) (e.g., header.tsx) Personコンポーネント以外はRSCとして、それぞれコンポーネント内でfetch関数を直接呼び出してデータの取得を行います。
loadingに関しては、React18からstableで提供され始めたSuspenseを用いることでコンポーネントのPromiseをキャッチしてfallbackの内容を返すことができます。
Next.js v13以降でページレベルでloadingを制御したい場合はloading.jsx(tsx)をpage.jsx(tsx)と同階層に置くことで対応できます。
(※上記のRSCではSuspenseの動作を確認するために、意図的にsleep関数を仕込んでいます)
また、error boundaryに関しては、ReactからはSuspenseのようにFunction Componentとして提供されているものはないようです。
しかし、独自で Class Component として定義する必要はなく、React のドキュメントでは、react-error-boundaryの利用などが代替手段として紹介されています。
もしNext.js v13以降でページレベルでerrorを制御したい場合は、Client Componentとしてerror.jsx(tsx)をloadingと同様page.jsx(tsx)と同階層に置くことで対応できます。
それでは、Personコンポーネント内のformを用いてユーザ名を更新してみましょう。
ここではServer Actionsを用いて更新処理を行います。(調査環境: Next.js v14.0.2)
Server Actionsの細かな説明は割愛しますが、/app/api内部でしていた処理と同等の処理を行っています。Server ActionsからORMを介して直接DBを更新する処理です。
ここで注目したいのがrevalidateTag("user");の部分です。
fetchの際のoptionとして{ next: { tags: [tag] } }が渡されたものに関しては、これがデータの再検証の際のキャッシュのタグとして紐付けられます。
Server Actionsでデータ更新後にrevalidateTag(tag);を行うとNext.js組み込みのData Cacheストレージからそのタグに紐づけられたキャッシュが再検証されて最新のデータに置き換わります。
結果
RSCのfetchを用いたときのデータ取得・更新の挙動です。

RSC, App Routerでのデータ取得
リクエストの重複
Request Memoization
ReactにはRequest Memoizationという機能が備わっており、fetchを用いたリクエストをメモ化し、キャッシュサーバへのリクエストの重複を排除してくれます。SWRやTanStack Queryで内部的に用いられていたContext Providerの仕組みがキャッシュによって実現されているイメージです。
さらに、Router Cacheという機能により、各ルートへのリクエスト結果がインメモリのクライアントサイドストレージにキャッシュされているため、次の描画までの時間(INP)も削減されます。
異なるページを行き来すると、キャッシュされていたRSC payloadが、独自のデータフォーマットで返却されていることがわかります。

クライアントサイドインメモリキャッシュのおかげでセッション期間中は都度サーバにアクセスしない
このほかにも、多くのキャッシュの仕組みによってNext.js App Routerでのデータフェッチは最適化されています。
全体の結果
今回の3シリーズの調査をまとめた結果です✉️
フェッチの分類
| RSC(in App Router) | getServerSideProps(in Pages Router) | SWR | TanStack Query | useEffect |
|---|---|---|---|---|
| サーバサイドフェッチ | サーバサイドフェッチ | クライアントサイドフェッチ | クライアントサイドフェッチ | クライアントサイドフェッチ |
- RSC: React Server Components
結局いつどれ使ったらいいの
| RSC(in App Router) | getServerSideProps(in Pages Router) | SWR | TanStack Query | useEffect | |
|---|---|---|---|---|---|
| CSR | ❌ | ❌ | ⭕️ | ⭕️ | 🔼 |
| SSR:各コンポーネントでデータフェッチを行う | ❌ | 各コンポーネントでのデータフェッチは想定されない(❌) | Hydrationの考慮が必要(⭕️)[1] | Hydrationの考慮が必要(⭕️)[1:1] | サーバサイドでデータフェッチができないとき(🔼) |
| SSR:SSR時にデータ取得 | ❌ | ⭕️(getServerSidePropsに限らず、該当SSRライブラリのAPIを使用) | サーバサイドでデータフェッチができないとき(⭕️) | サーバサイドでデータフェッチができないとき(⭕️) | サーバサイドでデータフェッチができないとき(🔼) |
| Next.js App Router | ⭕️ | ❌ | Client Components で利用可能(⭕️) | Client Components で利用可能(⭕️) | Client Components で利用可能(🔼) |
- RSC: React Server Components
- ⭕️: 利用可能/推奨
- 🔼: 利用可能/他のアプローチを推奨
- ❌: 利用不可
それぞれの特徴まとめ
| RSC(in App Router) | getServerSideProps(in Pages Router) | SWR | TanStack Query | useEffect | |
|---|---|---|---|---|---|
| フェッチの特徴 | コンポーネント単位でのデータフェッチ/ページ単位でのSSR | ページ単位でのデータフェッチ/ページ単位でのSSR | コンポーネント単位でのデータフェッチ/子コンポーネント単位でのレンダリング | コンポーネント単位でのデータフェッチ/子コンポーネント単位でのレンダリング | コンポーネント単位でのデータフェッチは基本的に行わない/useEffectを使用しているすべてのコンポーネントで起こる |
| キャッシュ | Request Memoizationによるリクエスト重複排除(@Server)/ Data Cacheによるリクエスト結果のキャッシュ(@Server)/ Full Route CacheによるHTMLとRSC payloadのキャッシュ(@Server)/ Router CacheによるRSC payloadのルートごとのキャッシュ(@Client) | 本番環境でのみ(⭕️) | ⭕️ | ⭕️ | ❌ |
| 状態表示(loading, 再検証など) | ⭕️[2] | ❌ | ⭕️ | ⭕️ | 難しい(❌) |
| リクエスト重複排除 | ⭕️ | ❌ | ⭕️ | ⭕️ | ❌ |
- ⭕️: できる
- ❌: できない
まとめ
自分の中で挙動や理解がまとまっていなかった、Reactにおけるさまざまなデータフェッチ・管理方法を広く浅くまとめることができて良い機会だったと思います。
まとめると、
- Next.jsなどのフレームワークを使用している場合は、組み込みのデータフェッチを利用する
- フレームワークを利用しない場合はSWRやTanStack Queryなど、クライアントサイドキャッシュを利用できるライブラリを検討する
- それ以外の場合・どちらも使えない場合は
useEffectで直接データフェッチをする
となり、useEffectの出番は稀になりそうです。
それぞれのデータフェッチ方法の個性を活かしつつ、適材適所で使っていきたいと思います!
OSSいつもありがとう!🙌🏻
参考
-
これらのクライアントサイドデータフェッチライブラリをSSRと組み合わせて使用する場合、
getServerSidePropsなどでデータをサーバ側でpre-fetchし、そのデータをSWRやTanStack Queryの初期データとして注入できます。
SSRでSWRを使用する
SSRでSWRを使用する - codesandbox
SSRでTanStack Queryを使用する ↩︎ ↩︎ -
App Routerで使用されているRSCについても状態表示は可能なのですが、筆者の感想としてはクライアントサイドデータフェッチライブラリと比較して煩雑さを感じる部分があります。SWRやTanStack Queryのようなクライアントサイドライブラリだと、独自実装せずともloading・再検証などの状態を戻り値として返却してくれるからです。RSC(in App Router)でも
SuspenseやError Boundaryを設けて状態表示はできるのですが、SWRやTanStack Queryの状態管理と比較すると細かな制御が難しい印象のため、「比較的煩雑」としました。 ↩︎
Discussion