🌨️

Reactのさまざまなデータフェッチ方法を比較して理解して正しく使用する - SSR + App Router Cache編

2023/11/24に公開

「Reactのさまざまなデータフェッチ方法を比較して理解して正しく使用する」シリーズの3記事目、最終記事です🌟

今回は「 Next.js Pages Router(SSR)でのデータフェッチとApp Routerでのデータフェッチ」について理解を深めていきます。

また、最後の全体の結果で「Reactのさまざまなデータフェッチ」シリーズの総括をしていきます。

  1. イントロ+useEffectを用いたデータフェッチ
  2. SWR・TanStack Queryを用いたデータフェッチ
  3. Pages Router(SSR)でのデータフェッチ+App Routerでのデータフェッチ+まとめ ← 👀この記事

Repository

以下は今シリーズで用いたリポジトリです。

🔽クライアントサイドフェッチの調査に用いたリポジトリ:React+Vite(useEffect, SWR・TanStack Query)
https://github.com/saku-1101/caching-swing-csr
🔽サーバーサイドフェッチの調査に用いたリポジトリ:Next.js Pages Router, App Router
https://github.com/saku-1101/caching-swing-pages
https://github.com/saku-1101/caching-swing

Pages Router(SSR)でのデータフェッチ

Next.jsのPages Routerでは標準で SSR (Server Side Rendering) 機能が提供されており、上手く活用することでパフォーマンス・SEOの両面で恩恵を受けられます。
Pre-rendering and Data Fetching
Rendering
SSRとCSRの比較
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することによって実現できます。
https://github.com/saku-1101/caching-swing-pages/blob/9f7495226371929c6e817265edf989ecf2e74d7e/src/pages/ssr-fetch/index.tsx#L19-L72
今回のサンプルでは、getServerSideProps の中で直接レンダリング時に必要となるデータを取得しています。
取得したデータをpropsプロパティを持つオブジェクトとしてreturnすることで、SSR時にそのデータがpropsとしてJSX(TSX)に注入されます。

API Routes との使い分け

ところで、getServerSideProps内でNext.jsのAPI Routesで定義したAPIを使わず、getRandomNumber()getUer()といったAPIの内部処理を直接使用したのはなぜだったのでしょうか?😶

もしgetServerSidePropsfetch('${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.

https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props#getserversideprops-or-api-routes

先ほどの例で考えてみると、getRandomNumber()getUer() と同等の処理内容を返す API Routes が存在していたとしても、それをgetServerSidePropsからfetch()などで呼び出すことは避け、内部ロジックのみを流用して直接実行することが推奨されます。

次の例のようにgetServerSidePropsから API Route で定義したAPIにリクエストを送ると、二重でリクエストが発生します。特別な理由がない限りはこのような書き方は避けたほうが望ましいです。

🔽 次のコードのように、getServerSideProps内でAPI Routeで定義したAPIにリクエストを送ると二重にリクエストを送ることになる

index.ts
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コンポーネントでユーザ名を更新してみましょう🤾🏻‍♀️
https://github.com/saku-1101/caching-swing-pages/blob/9f7495226371929c6e817265edf989ecf2e74d7e/src/pages/ssr-fetch/children/user.tsx#L6-L19
bodyformからのデータを付与したPOSTリクエストを/api/update/userに送ると、DBの値が更新されます。通常通りです。

更新したデータをUIに反映します。
https://github.com/saku-1101/caching-swing-pages/blob/9f7495226371929c6e817265edf989ecf2e74d7e/src/pages/ssr-fetch/children/user.tsx#L18
ここではNext.jsのPages Routerを使用しているのでnext/routerからエクスポートされているuseRouterの機能router.reload();を利用して再レンダリングをトリガーしました。

結果

getServerSidePropsを用いた時のデータ取得・更新の挙動です。
SSR data fetch
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のリクエストのキャッシュは本番環境でのみ、以下の設定を加えることで可能なようです。
(※今回は開発環境での調査のみ行なっているため、この機能は利用していません)
https://nextjs.org/docs/pages/building-your-application/deploying/production-checklist#caching

App Routerでのデータフェッチ

最後に、Next.js App Routerのキャッシュ機構を用いたデータフェッチと再検証を見ていきます。

App Routerでのデータフェッチの調査

RSC(React Server Components)をNext.js App Router環境で使用します。

(page.tsx)
https://github.com/saku-1101/caching-swing/blob/85aa6baca8ec4ef5f7148a5c57f4e6a5d0072877/src/app/prc-fetch/page.tsx#L8-L24
(e.g., header.tsx)
https://github.com/saku-1101/caching-swing/blob/main/src/app/prc-fetch/children/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の利用などが代替手段として紹介されています。
https://github.com/saku-1101/caching-swing/blob/main/src/app/prc-fetch/error.tsx
もしNext.js v13以降でページレベルでerrorを制御したい場合は、Client Componentとしてerror.jsx(tsx)loadingと同様page.jsx(tsx)と同階層に置くことで対応できます。

それでは、Personコンポーネント内のformを用いてユーザ名を更新してみましょう。
ここではServer Actionsを用いて更新処理を行います。(調査環境: Next.js v14.0.2)
https://github.com/saku-1101/caching-swing/blob/main/src/app/prc-fetch/actions/handleUpdateUserName.ts
https://github.com/saku-1101/caching-swing/blob/main/src/app/prc-fetch/children/user.tsx
Server Actionsの細かな説明は割愛しますが、/app/api内部でしていた処理と同等の処理を行っています。Server ActionsからORMを介して直接DBを更新する処理です。

ここで注目したいのがrevalidateTag("user");の部分です。
https://github.com/saku-1101/caching-swing/blob/6bba6e5f662018c0cc3bdb68fb58c09e9b3de3f5/src/app/prc-fetch/actions/handleUpdateUserName.ts#L14
https://github.com/saku-1101/caching-swing/blob/a5250ba30e0b790a4fdfa358444a520ca2e8c2b5/src/app/prc-fetch/children/form-output.tsx#L6-L8
fetchの際のoptionとして{ next: { tags: [tag] } }が渡されたものに関しては、これがデータの再検証の際のキャッシュのタグとして紐付けられます。
Server Actionsでデータ更新後にrevalidateTag(tag);を行うとNext.js組み込みのData Cacheストレージからそのタグに紐づけられたキャッシュが再検証されて最新のデータに置き換わります。

結果

RSCのfetchを用いたときのデータ取得・更新の挙動です。
fetch in App Router
RSC, App Routerでのデータ取得

リクエストの重複

Request Memoization

ReactにはRequest Memoizationという機能が備わっており、fetchを用いたリクエストをメモ化し、キャッシュサーバへのリクエストの重複を排除してくれます。SWRやTanStack Queryで内部的に用いられていたContext Providerの仕組みがキャッシュによって実現されているイメージです。

さらに、Router Cacheという機能により、各ルートへのリクエスト結果がインメモリのクライアントサイドストレージにキャッシュされているため、次の描画までの時間(INP)も削減されます。
異なるページを行き来すると、キャッシュされていたRSC payloadが、独自のデータフォーマットで返却されていることがわかります。
クライアントサイドインメモリキャッシュ
クライアントサイドインメモリキャッシュのおかげでセッション期間中は都度サーバにアクセスしない

このほかにも、多くのキャッシュの仕組みによってNext.js App Routerでのデータフェッチは最適化されています。
https://nextjs.org/docs/app/building-your-application/caching

全体の結果

今回の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におけるさまざまなデータフェッチ・管理方法を広く浅くまとめることができて良い機会だったと思います。

まとめると、

  1. Next.jsなどのフレームワークを使用している場合は、組み込みのデータフェッチを利用する
  2. フレームワークを利用しない場合はSWRやTanStack Queryなど、クライアントサイドキャッシュを利用できるライブラリを検討する
  3. それ以外の場合・どちらも使えない場合はuseEffectで直接データフェッチをする

となり、useEffectの出番は稀になりそうです。

それぞれのデータフェッチ方法の個性を活かしつつ、適材適所で使っていきたいと思います!
OSSいつもありがとう!🙌🏻

参考

https://zenn.dev/akfm/articles/next-app-router-client-cache#request-deduping

脚注
  1. これらのクライアントサイドデータフェッチライブラリをSSRと組み合わせて使用する場合、getServerSidePropsなどでデータをサーバ側でpre-fetchし、そのデータをSWRやTanStack Queryの初期データとして注入できます。
    SSRでSWRを使用する
    SSRでSWRを使用する - codesandbox
    SSRでTanStack Queryを使用する ↩︎ ↩︎

  2. App Routerで使用されているRSCについても状態表示は可能なのですが、筆者の感想としてはクライアントサイドデータフェッチライブラリと比較して煩雑さを感じる部分があります。SWRやTanStack Queryのようなクライアントサイドライブラリだと、独自実装せずともloading・再検証などの状態を戻り値として返却してくれるからです。RSC(in App Router)でもSuspenseError Boundaryを設けて状態表示はできるのですが、SWRやTanStack Queryの状態管理と比較すると細かな制御が難しい印象のため、「比較的煩雑」としました。 ↩︎

サイボウズ フロントエンド

Discussion