React, Next.jsについて理解を深める

React, Next.jsについておさらい
React, Next.jsについて、業務で必要になったためキャッチアップを行ったが、なぜ必要でどういう役割があるかなどが整理できていない(忘れてきた)ため、AIに質問したり、個別の記事などからおさらいする。

Reactについて
そもそもなぜ必要なのか?
理由1. コンポーネントベースで開発できるため、再利用しやすくコードの保守性が高まるから。
使用しないと、現代のWebアプリを作ろうとした時に複雑になってしまう。
理由2. コンポーネントの再利用ができるため、UIに一貫性を持たせやすくなる
これは使っていて実感できる。
理由3. 大規模アプリケーションではコードが理解しやすくなり、DOM操作もReact側がやってくれる
仮想DOMのため、コードで直性DOM操作をしなくて良いからコードは簡潔になる。
DOM操作をReactに任せることでバグも発生しずらい。
逆に直接DOM操作を行うようなコードは書いてはいけない。
React側が仮想DOMを管理し、変更された部分のみ更新するなどパフォーマンスの向上につながる。
Reactのみで実装した場合のレンダリング方法は?
Reactはクライアントサイドのライブラリのため、デフォルトでCSRのみサポートしている。
CSRは、DOMの構築もクライアントサイドで行うため、初期ロードに時間がかかる。
CSRの向き不向き
CSRが向いているシーン
- リアルタイム性が要求される場合
ページ更新時にページ全体ではなく特定のDOMのみを更新できるため、変更内容が即座に反映できる。
処理がほどんどクライアントサイドで行われるため、サーバーとの通信を最小にできる。
上記の理由により、サーバー側の負荷も抑えられる。
CSRが不向きなシーン
-
SEO重視のページ
初期HTMLにほとんど内容がなく、JSによってコンテンツが生成されるため、クローラーがJSを実行しない場合にサイトに関する情報がほとんど取得できない。 -
小規模な静的サイト
静的なサイトではReactによる恩恵が受けれず、過剰な実装となってしまう。
小規模なサイトにも関わらず、コンテンツのダウンロードサイズが大きくなってしまいページの表示に時間がかかってしまう。 -
パフォーマンスに制約がある場合
クライアントサイドで処理を行うため、利用する端末の性能が低い場合は向いていない。
また、同様の理由で初期表示速度が要求される場合も向いていない。
回線的に大量のJSコードがダウンロードできない場合も向いていない。

Next.jsについて
そもそもなぜ必要なのか?
Reactのみでは実現が難しい以下の機能を提供するため。
SSR
Reactのみではクライアント側でレンダリングを行っていたが、SSRによりリクエスト時にサーバー側でHTMLを生成することで初期表示を高速にできる。
SSG
ビルド時にHTMLを生成することで、CSRよりもページ表示が高速になる。
ファイルベースルーティング
Reactのみでは別途ライブラリを使用してルーティングを行う必要があるが、Next.jsによって自動でルーティングできる。
開発時にはファイルのディレクトリ構造がそのままパスになるため、ルーティングの設定を気にしなくて良い。
自動パフォーマンス最適化
next/imageコンポーネント
- WebPやAVIFなどの最新フォーマットを自動的に使用
- デバイスに応じた適切なサイズの画像を提供
- 画像の遅延読み込みを自動的に実行
- レイアウトシフトの防止
next/scriptコンポーネント
- 外部スクリプトの実行順序を制御
- strategyプロパティによる優先順位の自動設定
- パフォーマンスへの影響を最小限に抑制
Next.jsの使い方に沿っていないとこの恩恵を受けれない点で注意が必要かと。
SEO対策
SSR、SSGによってJSを実行しないクローラーに対応できる。
Next.jsについて勘違いしていたこと
Next.jsは、フロントエンドに特化したReactベースのフレームワークであり、APIサーバーではない。
サーバー機能を内包しているため勘違いしていた。
バックエンド機能を実装することも可能だが、推奨されていない。
このあたりについて、AIによる情報は以下のとおり
Next.jsサーバーの役割
主な機能
- Webブラウザへのコンテンツ配信を行います
- サーバーサイドレンダリング(SSR)の実行
- 静的サイト生成(SSG)の提供
- APIルートを通じた簡易的なAPI機能の提供
バックエンドとの関係
アーキテクチャ上の位置づけ
- バックエンドとは別の環境として動作します
- 既存のバックエンドと併用することが可能です
- 完全なバックエンドフレームワークではありません
推奨される使用方法
設計上の注意点
- 小規模なプロジェクトではNext.jsのみでバックエンド機能を実装することも可能です
- 重たい処理や精度が必要な処理は、専用のバックエンドサーバーに任せるべきです
- APIサーバーとしての利用は推奨されていません
この内容が頭に入っているだけでもだいぶNext.jsに対する理解は深まるはず。

Next.js(App Router)を使う上で知っておきたいこと
AIに聞いた内容だけでは、実装時にどうすべきか迷ってしまうため、ある程度の指針が自分の中で確立されるようにさらに調査した。
参考にしたもの
以下の動画でNext.jsのベストプラクティスについてキャッチアップした。
自分は動画の方が理解しやすいと改めて実感。
動画の元となっている記事は以下。
以降の項目はキャッチアップした内容のまとめ。
コンポーネントについて
デフォルトはServer Component
Next.jsでは、何も考えずにコンポーネントを作成した場合、Server Componentになる。
よって、Client Component
で動作するライブラリやhooksが使えないので注意が必要。
どうしてもClient Componentにしたい
ファイルの先頭にuse client
をつける。
ただ、この方法ではこのコンポーネント配下のコンポーネントもすべてClient Component
になってしまうため、推奨されない。
使うとしてもコンポーネントツリーの末端で使用する。
ではどうするのが良いかというと、
ファイル全体をClient Component
にするのではなく、Client Component
のchildrenとしてServer Component
を渡して使用する(Compositionパターン)。
Client Component
を使用する場面は?
- イベントハンドラ(
onClick()
,onChange()
)、hooksやブラウザAPIを使用する場合 -
React Server Components
に未対応な(Client Component
でしか使用できない)サードパーティライブラリを使用する場合 - RSC Payload転送量を削減したい場合
Server Component
の場合、レスポンスとしてRSC Payloadが毎回転送されてくるため、コンポーネントが大きいとその分転送量も増える。
これを解決するために、Server Component
ではデータフェッチのみ、Client Component
ではReactElement部分のみとすることでRSC Payloadの転送量を削減するために使用する(Container/Presentationalパターン)。
データフェッチについて
Server Component
で行う
データフェッチはReactのみの場合、サードパーティライブラリを使用したClient Component
でデータフェッチを行うことが推奨されているが、Next.jsを使用する場合はServer Component
で行う。
しかし、ユーザーの操作に伴うフェッチはServer Component
に向いていない。
なぜならServer Component
はSSRであり、UXを損ねるためである。
Server Actions
+ useActionState()
ユーザーの操作に伴うフェッチはClient Component
側でuseActionState()
を使用し、Server Actions
を呼び出す方法である。
この方法はデータ操作を伴うフェッチには対応していない。
データ操作を伴うデータフェッチ
ユーザーの操作に伴うフェッチと同様、データフェッチ自体はServer Actions
で行い、Client Component
でそれを使用する。
更新内容を反映するため、Server Actions
内でrevalidatePath()
やrevalidateTag()
を使用しキャッシュを再検証する必要がある。
Server Actions
に対応しているフォームライブラリとしてconformがある。
ただ、上記の方法ではリアルタイム性に弱いため、リアルタイム性が要求されるような場合はサードパーティライブラリを使用した実装を検討する。
キャッシュについて
Next.js App Routerではfetch
が拡張されており、それを使用した場合、v14ではデフォルトでキャッシュされるようになっていたが、v15以降ではデフォルトでキャッシュされなくなっている。
Next.jsには4つのキャッシュが存在する。
Request Memoization
同一のURL・同一のオプションリクエストは、1つのリクエストにまとめられる。
これにより同じデータフェッチを各コンポーネントで行なっていても重複したリクエストにはならない。
つまり、各コンポーネントでデータフェッチの記述ができるため、親コンポーネントからバケツリレーによるデータの受け渡しを行う必要がなくなる。
データフェッチ部分のみを関数化しておくと良い。
キャッシュ期間はリクエスト毎。
Data Cache
データフェッチの結果(JSON)をサーバー側で永続的にキャッシュする。
このキャッシュは、リクエストやユーザーを超えて共有されるため注意。
ユーザー固有の情報ではなく共通の項目に対するデータフェッチ時に使用する。
fetchのcacheオプションをforce-cache
とすることでData Cacheを有効にできる。
revalidateにより再検証の期間を指定することも可能。
再検証の期間を指定しても期間後の初回リクエストはキャッシュされたものになり、裏でキャッシュが更新される。
Full Route Cache
Static Rendering(SSG・IRSに相当)の場合、HTMLやRSC Payloadをキャッシュする。
ビルド時と再検証時にキャッシュされる。
頻繁にデータが更新されるようなユーザー固有の情報を扱うページでなければ、こちらを利用することが推奨される。
デフォルトはStatic Renderingだが、Dynamic Rendering(SSRに相当)の場合はキャッシュされない。
cookies()
/headers()
といったDynamic Functionsを使用すると、Dynamic Renderingになるので注意。
Router Cache
これのみクライアント側のキャッシュ。
セッション中のキャッシュのため、ブラウザを閉じると破棄される。
ユーザーが訪問したページのRSC Payload(Server Componentsのレンダリング結果)をキャッシュしている。ページ更新するとクリアされる。
<Link>
コンポーネントを用いたページ遷移では、静的ページの場合はデフォルトでリンクが見えた時点でprefetch(事前にロード)される。
prefetch属性をfalseにすることで無効にできる。
動的ページでは、動的な部分以外がprefetchされる。
キャッシュの期間はnextConfigのstaleTimesで制御できる。
キャッシュの再検証は以下の方法で可能。
-
Client Component
でrouter.refresh()
を呼び出す - Server Actionsで
revalidatePath()
/revalidateTag()
を呼び出す(データ操作を伴うデータフェッチでも言及) - Server Actionsで
cookies.set()
/cookies.delete()
を呼び出す
Router Cacheの挙動は、ドキュメントにはないものもあるようなので目を通しておくのが良いかも。