【学習用】Next.js(AppRouter)について要点を学び直すのでまとめた

投稿日:2024/12/03 最終更新日:2024/12/21

【学習用】Next.js(AppRouter)について要点を学び直すのでまとめた

コンポーネント

Client Components

・クライアントサイドでレンダリングされるコンポーネント
・ファイルの先頭に”use client”をつけると使用できる
・Hooksを利用する際(useState, useEffect)
・イベントハンドラを利用する際(onClick, onChange)
・ブラウザAPIを利用する際(localStorage, sessionStorage)
・フォーム / 検索などHooksを利用するといったクライアント操作が多いコンポーネント
・子コンポーネントは自動的にClientComponentになってしまうので注意
・使用の際は末端の子コンポーネントに使用する

※ServerActionsを利用すればフォームはサーバーでも利用できる

Server Components

・サーバーサイドでレンダリングされるコンポーネント
・デフォルトで利用できる(”use server”をつけて明示することもできる)
・データフェッチが高速(APIなどにサーバーは物理的に近いから)

・サーバー側でSSRされるからJSバンドルサイズが小さい
・クライアントで処理するJSサイズが小さいということ
・バンドルサイズが大きいと初期ロード時間が大きくなる
・SRCペイロードが大きくなりそうであれば、あえてClientComponentsを使用したほうが良い

・クライアントサイドのスペックに依存しない
・SEOの向上(事前レンダリング、動的メタデータ設定)
・セキュリティの強化(APIやDB操作がサーバーサイド処理になるため)
・Compositionパターンを利用すれば子コンポーネントへのClientComponents伝播を回避できる

・Server Componentsは多段階計算を行う
・プログラムの評価を多段階に分けて処理する機構
・動的にコード生成をして走らせる機能を備えた複数計算をするステージのある体系

・多段階計算に加えて以下の性質を伴っていることが重要視される
・①サーバー(stage0)→クライアント(stage1)の順で実行しても静的検査により安全性確保がなされていること
・②異なる段階のプログラムが似たような構文や意味論を持つこと(RSCとRCCの関係はそうなっている)

・PHPの実行イメージと近い(サーバー側でPHPを実行→クライアント側でHTML表示)=多段階計算
・RSCがPHPよりも優れている点:①と②がReactによって書かれていること
・PHPはクライアント側での動的操作にはJSが必要だが、RSCの場合Reactの記述のみで実装できる

・RSCではサーバー(stage0)→クライアント(stage1)の過程でHTML生成をするため逆はできない
・なのでuse clientは末端のコンポーネントで指定すべきでコンポーネントの粒度も細かい方が良い
・逆の動きはNext.jsのビルド時に弾かれるため防ぐことはできる

・イメージとしてはstate1のみだったのが前段階にstate0が追加された
・Next.jsではstate0を基本としてクライアント用にする場合はuse clientに移す
・本来ならSCの方が処理上有利だがstate利用などを考慮してパフォーマンス悪化を受容する行為とも考えられる

・stage0の実行イメージ
・SSG的なページであればビルド時に実行すれば良い
・SSR的なページであればページアクセス時にstage0が実行すれば良い
※stage1はいずれもクライアント側で実行

データフェッチ

従来の考え方とそのデメリット

・従来はデータフェッチをクライアントサイドで行うことを良しとされていた
・そのためのライブラリや実装パターンが多くある(SWRやtRPCなど)

・クライアントとサーバー間の通信は物理的距離やネットワーク環境に左右されるため通信回数が少ない方が良い
・ただクライアントサイドの処理は通信回数が多くなりがち
・SWRなどはサードパーティーライブラリであるため堅牢性が求められるデータフェッチにおいて対策コストがかかる
・それによるバンドルサイズの増加も起こりやすい(多くのライブラリを使用するため)

・それらを踏まえてReactのサーバー活用をよりできるようにすべきと考えてRSCが生まれた
・RSCならサーバー間による高速なデータ通信の恩恵を受けられる
・非同期関数をサポートしているためサードパーティーライブラリなしで実装できる(fetch関数)
・データフェッチをサーバーサイド処理するためセキュリティリスクを下げられる
・サードパーティーライブラリを使用しないためHTMLやRSC Payloadのみとなりバンドルサイズの削減ができる

・GraphQLの利用は知見やライブラリ不足によりバンドルサイズの増加が見込まれるためRSCと相性が悪い

fetch関数

・fetch関数はNext.jsでは拡張されている
・v14以前はSSGがデフォだったが以降はSSRがデフォ
・オプトインでSSG/SSR/ISRを選択可能
・キャッシュ設定が簡単(引数設定でできる)
・Request memoizationで重複したリクエストを排除してくれる

ORMの利用

・Prismaなどのマッパーのこと
・RouteHandlerで書く必要はない(/app/api/~ のこと)=通信回数が増えてしまう
→ 結果的に使うことはある気がします(全く使わないは厳しい気がする)

サードパーティーライブラリの利用(ClientComponentの場合)

・useSWR()の利用
・Tanstack Query
・useEffectでfetch関数を利用するのは非推奨(キャッシュの設定ができない)
・データフェッチはサーバーレンダリングで使用するのが今主流だからあまり使わないほうが良い
・JSバンドルサイズが増加する原因にもなる

並行データフェッチ

・データ取得と依存関係のない部分は並行処理した方が速度的には良い
・基本はコンポーネントに分割をすると自動で並行処理になる

water fall data fetch

・データ間に依存関係がない場合はデータフェッチごとにコンポーネント分割をする
・データフェッチ同士に依存関係がある場合は並行処理できず同期的になる
・非同期コンポーネントは兄弟要素である場合に並行処理される
・データの依存関係が不明確な場合はPromise.allを使用すると各処理が並行処理になる

async function Page() {
  const [items, posts] = await Promise.all([
    fetch(`https://sample.com/items/${id}`).then((res) => res.json()),
    fetch(`https://sample.com/posts/${id}`).then((res) => res.json()),
  ]);
  // ...
};

・兄弟要素でなく親子関係にしないといけない場合はpreloadパターンを使用すると並行処理ができる

N+1問題について

・データフェッチの粒度を細かくしていくと発生する問題
・DataLoaderを使用してデータアクセスをキャッシュする

ベストプラクティスについて

・コンポーネントと基本理由は同じ
・リアルタイム性に弱い(その場合はReact)
・楽観的UI更新を使えない

・フォームなどはServerActionsでサーバー上でも動かせる
・JSが無効でも動く(プログレッシブエンハンスメント)

・ServerComponentではリアルタイムバリデーション(Zodなど)が困難

・Container/Presentationパターンを意識する
・Container:データ取得層
・Presentation:表示部分
・これをわけて実装することを意識する
・React Testing LibraryはServerComponent非対応のため分けたほうがテストしやすい
・ Containerから実装してテスト(Jest)+その後にPresentationを実装してテスト(React Testing Library)
・RSCペイロード(静的DOM)の量が少なくなる

キャッシュ

前提

・データフェッチはコロケーションすることが推奨されている
・ただ末端のコンポーネントで行う場合は複数箇所でデータフェッチを行うことでリクエストの重複が起こる
・なので、リクエストをメモ化することが必要

Request memoization

・メモ化
・同じリクエストは重複排除される
・[A,B][A,C][B,C][A,B,C]の処理を[A,B,C]に自動的にまとめてくれる
・リクエスト→Request memoizationのキャッシュ有無→なければDataFetch→キャッシュする
・一致するURLのみ有効でエンドポイントが異なるとメモ化できない(オプションを含めた同一URLでないといけない)
・フェッチする関数はファイルなどで分離させてData Access Layerにまとめる
・ORMで作成した関数はメモ化できない
・RouteHandler内もメモ化できない
・できない場合はReact Cacheを利用する

import { cache } from "react";
import db from "~/db";

export const getInfo = cache(async(id: string) => {
  const info = await db.item.findUnique({
    where: {
      id: id,
    },
  });
}); 

・最下層のコンポーネントでfetchしても問題ない(動的メタデータでも有効)
・キャッシュ期間は永続的ではない(サーバーを落としたり再ビルドをすると勝手に破棄される)

・データフェッチは基本的にServer Componentsで利用することが推奨される

// Client Compomnentsでimportするとエラーを出してくれる
import "server-only";

export async function getProduct(id: string) {
  ~~
);

Data Cache

・デフォルトで有効
・アプリ全体に関わるキャッシュ(ユーザー全体に関わる)
・ユーザー固有の情報に紐付くデータの場合は使用してはならない
・JSON形式
・永続的なものなので取り扱いは注意(サーバーの再起動や再ビルドでも生きている)
・fetch関数で設定する(デフォルトではキャッシュしない)

// キャッシュあり
fetch("https://~~", { cache: "force-cache" })

// デフォルト、キャッシュなし
fetch("https://~~", { cache: "no-store" })

// キャッシュあり、時間設定で更新
fetch("https://~~", { cache: { next: { revalidate: 100 } } }

・force-cache:Request Memolazationのチェック→なければData Cacheのチェック→なければソース→全部にキャッシュ
・no-store:Request Memolazationのチェック→Data Cacheはスキップ→なければソース→RMにキャッシュ
・最新データを取得したい場合はno-storeにする(デフォルト)とSSRになる
・デフォルトはno-storeだが明示的に指定しないとdynamic renderingにならないため注意する

unstable_cache()を使用するとDBアクセスでData Cacheを利用できる

import { getUser } from "./fetcher";
import { unstable_cache } from "next/cache";

const getCachedUser = unstable_cache(
  getUser, // DBアクセス
  ["my-app-user"], // key array
  {
    tags: ["users"], // cache revalidate tags
    revalidate: 60, // revalidate time(s)
  },
);

export default async function Component({ userID }) {
  const user = await getCachedUser(userID);
  // ...
}

・SSG/SSR/ISRに関係する
・revalidateをして再検証することもできる(ISR)

Full Route Cache

・デフォルトで有効
・ページ全体のキャッシュ
・HTMLやSRCペイロードなどをキャッシュ
・永続的なものなので取り扱いは注意(ユーザー間で共有)
・SSG/ISRのみ利用可能(SSRは×)
・AppRouterはStaticRenderingを推奨している
・Full Route Cacheでページ全体を自動キャッシュ(とにかく早い)
・Dynamic Function(cookies / headers)を利用すると自動的に Dynamic Renderingになる

Router Cache

・ナビゲーション用のキャッシュ
・ページを訪問するとSRCペイロードが自動キャッシュされる
・クライアント側のメモリ内でキャッシュされる
・ない場合はServerリクエストをする

・production環境のみ有効

・Linkコンポーネントを利用すると自動的に使用可能

・静的ページの場合Linkコンポーネントはprefetchがデフォルトでtrueのため遷移先のプリロードがされている
・動的ページの場合共通レイアウト(動的でない部分)はprefetchされている(動的な部分はloading.jsを使用)

・キャッシュを無効にする場合はprefetchをfalseにするか、router.refleshを使用する
・キャッシュ期間はブラウザタブを閉じるまで(セッション中のみ)

・静的ページの場合はデフォルトで300秒(5分)
・動的ページの場合はデフォルトで30秒

staleTimesを使用すると設定できる(next.config.js)

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 0, // default: 30
      static: 180, // default: 300
    },
  },
};

export default nextConfig;

staleTimes以外でRouter Cacheを破棄するにはrouter.refresh()を利用する
・ブラウザバックの時はstaleTimesの値に関わらずキャッシュが適用されるので注意

ベストプラクティス

・予期しないキャッシュを避けること
・キャッシュの影響範囲を理解しておくこと
・ドキュメントで情報を追うこと
・Remixの利用も検討する(キャッシュの拡張がされていないフレームワーク)

Custom Next.js Cache Handlerについて

・Self-hostingでは必須
next.config.jscacheMaxMemorySize: 0を指定することでNext.jsのインメモリキャッシュを無効にする
・デフォルトではファイルキャッシュから読み取った値をインメモリにキャッシュとして保存するから
・neshcaライブラリを使用するとRedisを用いたキャッシュ永続化を割と簡単に実装できる

レンダリング

・サーバーレンダリングがデフォルト
・クライアントに負荷を掛けない

Static Rendering

・SSG
・とにかく早い
・静的ページにて有効
・更新の少ないページに使用する

・ISR
・キャッシュの更新時間を指定可能
・静的動的の中間のイメージ

Dynamic Rendering

・初期ロードはClient Componentよりも早い傾向がある
・リクエスト時に都度サーバーでレンダリング
・キャッシュはされない
・Dynamic Function(cookies / headers)を利用すると自動的に Dynamic Renderingになる

・Prismaでデータ更新時に再レンダリングを走らせたい場合はrevalidateTagを使用する

〜〜
import prisma from '@/~/prisma';
import { revalidateTag } from 'next/cache';

export default function FormComponent() {
  const formAction = async(formData: FormData) => {
    "use server";
    await prisma.data.create({
      data: {
        id: `post_${Math.floor(Math.random()*9999)+1000}`,
        content: formData.get("content") as string,
      }
    });
    // revalidateTagを使用することでサーバーコンポーネントで再レンダリングを走らせることが出来る
    revalidateTag("data");
  };

  return (
    // フォームコンポーネントを記述する
  );
};

Suspence / Streaming

・ストリーミングとは?
・SSR時に利用される
・段階的にUIやデータを提供する仕組み
・見えていない部分はフォールバックUIを表示する

・提供できるコンポーネントは並行処理される(待ち時間が短縮される)
・TTIB、FCP、TTIの改善になる
・Suspenceなど

StreamingとSEO

・SEOは従来静的表示されないコンテンツは評価対象にならないとみなされてきた
・Vercelの調査によるとStreaming SSRがSEOに与える影響はほぼないとされる(18時間あればインデックスされる)

Pertial Pre Rendering

・v15に試験的導入されている
・Partial = 部分的

・TTFBの観点であればSSGの方が有利だがfetch処理のシンプルさやHTTPラウンドトリップを考えるとStreaming SSRの方が良い
・両者の良さを活かしたのがPPR
・Streaming SSRを進化させたものでページをStatic Renderingとしつつ部分的にDynamic Renderingにすることが可能
・SSGやISRのページの一部をSSRを組み込むようなイメージ

ppr shell

・Suspenceの境界の外側がStatic Rendering、Suspenceの内側がDynamic Renderingとなる
・Streaming SSRのSuspenceの外側におけるリクエストの処理

  • ①Server Conponentsを実行
  • ②Client Componentsを実行
  • ③上記の結果をもとにHTMLを生成
  • レスポンスを返す

・PPRでは①〜③をビルド時に実行して静的化する(高速表示ができる)

・ページ単位からUI単位でのレンダリング方式になるかもしれない(SSGかSSRかを考えなくても良くなるかも)
・「どこまでをStaticに、どこからをDynamicにするか」

・デメリットもいくつか考えられる模様
・ページが必ず200になってしまうこと(Streaming SSRの場合はHTTP Statusで監視をするのは不完全、エラー発生率なども見る必要がある)
・CDNキャッシュは使えない(CDNはリクエストごとでのキャッシュを想定しているため)
・ページ全体をSSGに出来るのであればその方が有利になる

ベストプラクティス

・実装箇所を元にレンダリング方式をページ単位で判断すべし
・デフォルトではStatic Renderingを推奨している(必要によってDynamic Rendering)
・キャッシュもどのデータをキャッシュするかすべきかを意識すべし

メタデータ

静的メタデータ

・layout.tsxまたはpage.tsxで使用する

import type { Metadata } from 'next'

export const metadata: Metadata = {
 title: '...',
 description: '...',
}

・TDが静的に生成されて表示される

動的メタデータ

・ServerComponentのみで利用可能

import type { Metadata, ResolvingMetadata } from 'next'

type Props = {
 params: Promise<{ id: string }>
 searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
 // ルートパラメータを読み取る
 const id = (await params).id
 // データをフェッチする
 const product = await fetch(`https://.../${id}`).then((res) => res.json())
 // 親メタデータにアクセスして拡張する(置換しない)ことができます
 const previousImages = (await parent).openGraph?.images || []

 return {
   title: product.title,
   openGraph: {
     images: ['/some-specific-page-image.jpg', ...previousImages],
   },
 };
};

export default function Page({ params, searchParams }: Props) {}

・fetchリクエストはメモ化される(使用できない場合はReact Cacheを使用する)

・ストリーミング前にデータフェッチされるため動的なメタデータの変更は保証されている

ミドルウェア

ミドルウェアとは

・リクエストが完了する前にコードの実行をすることができる
・リライティング、リダイレクト、リクエスト、レスポンスヘッダの変更などを行うことができる
・プロジェクト内のすべてのルートに対して呼び出される

・下記のようなときに使用が想定される
・認証や認可前など特定ページやAPIへのアクセスの前にユーザー確認やセッションCookieの認証などを行うとき
・ユーザーロールなど特定条件に基づいてサーバーサイドでのリダイレクトを行いたいとき
・リクエストプロパティに基づいてAPIルートやページのパスを書き換えたいとき

・使用が想定されない場合
・データのフェッチと操作をしたいとき
・重い計算などの処理を行いたいとき
・セッションの管理をしたいとき
・DBの操作を行いたいとき

マッチャーで指定可能

・マッチャーを指定することで特定パスで実行するMiddlewareをフィルタリングすることができる

export const config = {
  matcher: '/about/:path*',
}

・配列構文にすれば複数パスを指定できる

export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

・マッチャーを使用して/other/~~のリクエストがあった場合はすべてトップページにリダイレクトさせる例

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL("/", request.url));
}

export const config = {
  matcher: "/other/:path*",
};

注意点

・マッチャーは定数でなければいけない(変数は無視される)

参考文献

https://zenn.dev/akfm/books/nextjs-basic-principle/viewer/intro
https://www.youtube.com/watch?v=Ca1h3KUfQ5k&t=416s
https://zenn.dev/oreo2990/articles/280d39a45c203e
https://zenn.dev/akfm/articles/nextjs-partial-pre-rendering
https://zenn.dev/akfm/articles/nextjs-cache-handler-redis
https://zenn.dev/uhyo/articles/react-server-components-multi-stage