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 RouterPages 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/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