Open24

いい加減Next入門する

ぱんだぱんだ

とりあえず以下の実行でプロジェクト作成

npx create-next-app@latest

TypeScriptはもちろん使う。ESLintは今はPrettierと合わせてBiomeで置き換えしちゃったほうがいいのかな。CSSはこだわりないのと使い慣れてはいるのでTailwind。srcディレクトリ使うかはどっちだろう。デフォルト値が使わないになってたので一旦使わないに。App Router。TurbopackはRspackでいいんですか?import aliasはそのまま。

できたら

npm run dev

でサーバーをローカルに起動できる。

ぱんだぱんだ

pnpm使ってみる

pnpm dlx create-next-app@latest
ぱんだぱんだ

Biome

https://biomejs.dev/ja/

pnpm add --save-dev --save-exact @biomejs/biome

VSCode拡張

https://marketplace.visualstudio.com/items?itemName=biomejs.biome

以下のコマンドで設定ファイルを生成できるがBiomeのデフォルト設定で満足しているなら作らなくても良い

pnpm exec biome init

Next.jsのプロジェクトにおいてLintはESLintのみを今のところサポートしていてプロジェクト作成時にESLintを有効にすると以下のような設定ファイルが作成される。

import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [
  ...compat.extends("next/core-web-vitals", "next/typescript"),
];

export default eslintConfig;

next/core-web-vitalsnext/typescriptのルールが適用されるのでとりあえずであればこれで十分そう。

Biomeを使いたい場合、ESLintもBiomeに寄せたいと思うがBiomeで上記のNext用のLinterルールを適用するのができない(もしくはめんどくさそう)ので、現状Lintの部分はESLintを使うのが無難そう。

フォーマットに関してはPrettierからBiomeにしてもほぼほぼ問題なさそうなのでESLint + Biomeもしくは従来通りESLint + Prettierのどちらかを選択すればよさそう。

本当はBiome1本でNextプロジェクトもカバーできるといいね(あとVueの完全サポート)

Biome + ESLint

とりあえず、なるべく最小限の労力でBiome + ESLintを設定してみる。フォーマットはデフォルト設定のBiomeに任せて、LintはNext用のルールがあったりするのでESLintに寄せる。LintはBiomeのLintとかぶっているのもあるだろうからなるべくBiomeに寄せたほうが早いのだろうが設定ファイルをなるべくいじりたくないのでLintはESLintに全部やってもらう

と思ったけど、BiomeのサポートでNextのプラグインでやってるような解析もできるみたい。例えば、<Image>が使用されていないときのリントルールはBiomeにあった

https://biomejs.dev/linter/rules/no-img-element/

全部あるかまでは見てないけど既存の作り込んだESLintがあるわけではないのならもうBiomeデフォルトでいいかになった

  • Biomeの設定はデフォルト
  • package.jsonに以下を記載
    "lint": "biome lint --write src/*",
    "format": "biome format --write src/*",
    "check": "biome check --write src/*"

CIで実行するならそれ用のアクションが公式からある

https://biomejs.dev/ja/recipes/continuous-integration/

Git Hooksも使いたくなるかもしれない。もし使うなら公式にも記載のLefthookが良さそう

https://biomejs.dev/ja/recipes/git-hooks/

settings.jsonには以下を記載しておけば良さそう

  "editor.codeActionsOnSave": {
    "source.fixAll.biome": "explicit", // 保存時に修正
    "source.organizeImports.biome": "explicit", // import並び替え
  },

書いてて気づいたけどoxlintがもう次世代の高速Linterとして登場してるのか、、

ぱんだぱんだ

Rspack と Turbopack

Vercelは従来のWebpackからより高速なTurbopackの開発を行っており執筆時点でv15.0.0でdevサーバーでの利用がstable、buildでの利用がv15.3.0でexperimentalとして提供されている。

Turbopackの利用は以下のように--turbopackオプションをつけるだけ。

next dev --turbopack

プラグインなどを組み込んでカスタムしたい場合はwebpackではnext.config.tsなどに以下のように書くことでカスタムすることが可能だった。

module.exports = {
  webpack: (
    config,
    { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack }
  ) => {
    // Important: return the modified config
    return config
  },
}

Turbopackを利用する場合は同様にnext.config.tsなどにwebpackと同じ要領で書くことができる。

RspackとはTurbopack同様Rust製の高速なwebpack互換のバンドラー。Turbnopackとの違いはRspackの方がWebpackとの互換性が維持されているため、少し前の構成としてよくあったWebpack + Babel + Reactみたいなプロダクトでの移行先として取り上げられることが多いようだ。

RspackはWebpackとの高い互換性のためNextでの利用もできるだろうというのはみんなうっすら感じていたようだけどVercelがRspackを受け入れドキュメントに記載するまでになるのは予想していなかったようだ。

執筆時点でのでVercelのスタンスとしてはTurbopackはまだ完全にstableではないため、段階的なTurbopackへの移行手段としてまずはRuspackでのNextの利用という道筋を示してくれているようだ。

まあ、なのでどっちを使うというかどっちも使える状態にあって最終的にはTurbopack使って欲しいけど今のところWebpackとの互換性ではRspackの方が高いのでWebpackからの移行を進めたい人は段階的な移行先としてRspack使えるようみたいな感じだろうか。

どっちにしろ執筆時点ではTurbopackは開発サーバーまでがstableでRspackもexperimentalな機能として紹介されているので何とも言えないがどっちも十分使用できる状態な気はしている。

ぱんだぱんだ

このスクラップ書いている時点でのバージョン

  "dependencies": {
    "next": "15.4.6",
    "react": "19.1.0",
    "react-dom": "19.1.0"
  },
ぱんだぱんだ

app directory

Next.jsはファイルベースルーティングを採用しているのでファイルをどのように配置するかでルーティングを設定することができる。

App Routerにおいてはappディレクトリ配下にファイルを配置する。appディレクトリはプロジェクトルートに配置してもいいしsrcディレクトリ配下に置いてもいい。

srcディレクトリの有無はプロジェクト作成時のオプションでデフォルトは無になっていたがsrcディレクトリがある利点としては、アプリケーションコードと設定関連のコードを分離できる。フロントのプロジェクトはなんとなくconfigファイル多くなる気がしなくもないので分離できていたほうが良さそうに思う。さらに、ディレクトリ構成をどうするかにもよるがcomponentsディレクトリやlibディレクトリみたいなappディレクトリ以外にもディレクトリを配置するならプロジェクトルートに配置すると設定ファイルとともにごちゃごちゃ感が強まる気もするのでよりsrcディレクトリを配置したほうが見やすくなりそう。

話が逸れたがファイルのルーティングはappディレクトリ配下にファイルを置く。

で、appディレクトリ内には以下の種類のファイルをおける。

layout

NextのApp Routerにおいてはまずlayout.tsxのようなファイルを設置し、コンテンツ部分に関しては後述のpageコンポーネントを使う。

layoutファイルはネストして配置することができ、appディレクトリ直下のlayoutファイルがroot layoutとなる。

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

ルートのlayoutファイルは必須だが、それ以降のパスにlayoutファイルを配置するかは任意。

ぱんだぱんだ

public ディレクトリ

静的ファイル置き場。アプリケーションコードからは以下のようにpublic配下の静的ファイルにアクセスできる。

import Image from 'next/image'
 
export default function Page() {
  return <Image src="/profile.png" alt="Profile" width={100} height={100} />
}
ぱんだぱんだ

インポートのパスについて

tsconfig.jsonに以下のように記載することができる。

		"baseUrl": "src/",
		"paths": {
			"@/components/*": ["components/*"]
		}

baseUrlを設定すると相対パスによるインポートの基準を設定することができる。pathsを設定すると@から始まるインポートをすることができる。

ぱんだぱんだ

ナビゲーション

Nextgではリンク先のコンポーネントはサーバー側でレンダリングされる。これはページ遷移する前にサーバーからの応答を待つ必要があるということ。Nextでは組み込みのLinkコンポーネントを使うことでprefetchやstreamingなどを駆使してページ応答の高速性を高めている。

Nextにおいてナビゲーションを理解するには以下について理解していると良い。

  • Server Rendering
  • Prefetching
  • Streaming
  • Client-side transitions

Server Rendering

NextにおけるlayoutとpageコンポーネントはデフォルトではReact Server Component(RSC)です。サーバーでのレンダリングはビルド時またはrevalidation時にレンダリングされるstatic renderingとclientからのリクエストで動的にレンダリングするdynamic renderingの2種類に分類される。

stagtic renderingはSSG, ISRと呼ばれていたものでdynamic renderingはSSRに分類されるであってる??

ここらへんはまたあとでまとめたいけどだいたいあってそう。App Rouoterは部分的にdynamic renderingの動的コンポーネントを静的なコンポーネントと合わせて使用することができるようになっていたり、レンダリングの方法をgetServerSidePropsのような明示的な指定からNext側が判別するような仕組みに変わってるなどしていそう。

Prefetching

Linkコンポーネントをホバーしたら事前にリンク先のコンポーネントを取得するprefetchがはしる。prefetchされるのは静的なコンポーネントと動的コンポーネントの場合はprefetchがスキップされるかもしくはローディングやスケルトンなどを事前にprefetchして高速に表示することができる。

Streaming

動的コンポーネントの場合、データの取得が終わり準備ができたコンポーネントから順に表示することができる。RSCの場合、サーバーからの応答をまたないといけないためLoading表示すらもサーバーからの応答を待たなければならないが、Streamingを使うことで部分的なprefetchが可能。つまり、共通の静的コンテンツやスケルトンを事前にサーバーから取得しておくことができるということ。

これは内部的にはSuspenseを使いpageコンポーネントをラップしている。より画面遷移の体験を向上させるには次に説明するClient-side transitionを使うことができる。

Client-side transitions

元来ページ遷移では状態やスクロール位置がリセットされるがNextのLinkコンポーネントでは動的にコンテンツを置き換え、ページ遷移したように見せるためこれらの問題を回避する。

これはいわゆるSPAにおけるルーティングで言われていた話と同じこと?

Nextではこれらの仕組みを使ってナビゲーションを高速にしているがそれでも遅いと感じることがあるかもしれない。

  • 動的ルートでローディングコンポーネントを使っていない
    • ローディングコンポーネントが配置されていれば内部的にSuspenseが使われてローディングを表示している間にコンポーネントをfetchできる
  • generateStaticParamsを使わずにdynamic segmentsを使っている場合prefetchできるのにprefetchが実行されない
    • あとでやる
  • 低速ネットワーク
    • useLinkStatusの話。あとでやる
  • prefetchの無効
    • でかいテーブルでprefetchがすべて有効だと逆にパフォーマンスの低下につながる可能性もある
  • ビルドサイズ
    • Linkはクライアントコンポーネントなので最初に訪問したときの表示にはJSのサイズが関係してくるよねという話だと思う

Native History API

window.history.pushStateとwindow.history.replaceStateを使ってブラウザの履歴を操作できるがこれはNext.js Routerに統合されておりusePathnameとuseSearchParamasを使って操作できる。

ぱんだぱんだ

client componentとserver component

layoutとpageはサーバーコンポーネントとなるのでサーバー側でレンダリングされる。クライントコンポーネントを使うときは主に以下のような場合

  • Stateやイベントハンドラーを扱うとき
  • useEffectのようなライフサイクルを伴うとき
  • ブラウザ固有のAPIを使うとき
  • カスタムフックを使うとき

サーバーコンポーネントを使うとデータフェッチやDBアクセス、APIキーのような秘匿情報の扱い、clientに送るJSサイズの削減などができるようになる。

サーバーコンポーネントからクライアントコンポーネントへはPropsを渡すことでデータを受け渡すことができるよ。

use Hookを使うとサーバーコンポーネントからクライアントコンポーネントにStreamデータを渡せるよ。

clientコンポーネントはPropsとしてサーバーコンポーネントを受け取ることもできるよ。

Reactのcontextを使いたい場合、サーバーコンポーネントが対応していないのでuse clientを付けたProviderコンポーネントを作成してサーバーコンポーネントを囲ってあげればcontextの値を使えるよ

サードパーティー製のコンポーネントを使う場合、RSCに対応していればいいが、対応していない場合、clientコンポーネントであることを明示するために薄いラッパーコンポーネントを作る必要がある

JSのモジュールはclient、server問わずに共通で使うことができてしまう。そのため環境変数にある秘匿情報を扱うようなサーバー処理をclientコンポーネントからでも呼び出せてしまいます。

実際にはフロントからNEXT_PUBLIC_がついていない環境変数を呼び出してもその値は空になるが、サーバーからのみ呼び出す処理には明示的にserver-onlyを記載することができる。

ぱんだぱんだ

PPR Partial Prerendering

PPRとは静的レンダリングと動的レンダリングを組み合わせ、部分的に動的レンダリングを可能とする。動的レンダリングはリクエスト時にHTMLが生成され、これは従来SSRと呼ばれていたレンダリング手法である。App RouterのNextでは以下が使われている場合、自動的に動的レンダリングを選択する。

  • cookies
  • headers
  • connection
  • draftMode
  • searchParams
  • unstable_noStore
  • fetch (cache: no-store)

上記が使われているコンポーネントをpageが含んでいるとdynamic rendaringがページ全体として選択されてしまうが、本当にdynamic rendaringが必要な場所だけをSuspenseで囲うことで部分的にdynamic rendaringを可能としたのがPPR。Suspenseは動的コンポーネントをカプセル化し静的コンポーネントとの境界を作る。

PPRに関しては以下のzenn記事などが非常に参考になる

https://zenn.dev/akfm/articles/nextjs-partial-pre-rendering

https://zenn.dev/akfm/articles/ppr-vs-islands-architecture

動的データと静的データが混在するpageをレンダリングする手法は従来

  • SSG + Client fetch
  • Stream SSR

の2つの手法しかなかったがPPRはその2つに変わる新しい手法。PPRを使うことで動的データと静的データが混在するpageにおいても、1回のリクエストで即在にレンダリング済みのHTMLを表示することができ、データが準備できしだい表示されるコンポーネントを置き換えることが可能。

Stream SSRとPPRの違いって現状experimentalなフラグを有効にするかどうかくらいで実装方法に違いはないってことであってる?であれば現状はexperimentalな機能だけど将来的にはPPRが選択されるようになるためStream SSRのパターンはなくなるってことかな??

なんにせよ、このPPRについて学ぶとSSRやSSG, ISRといったレンダリング方法を区別するのは確かに意味がなくなってきたんだなと感じるし、レンダリング手法に関してはdynamicとstaticなレンダリング手法がページ単位ではなくコンポーネント単位になってるなと思う。そして、そのdynamicとstaticの境界をSuspenseで実現しており、それがPPRなんだと。

ぱんだぱんだ

Fetching Data

データフェッチはサーバーコンポーネントでもクライアントコンポーネントでもStreamingコンポーネントでも行われる。

サーバーコンポーネントではfetch APIかORMなどを使って直接DBアクセスするなどがある。デフォルトではfetchの結果はキャッシュされないが、前述のLinkコンポーネントを使ったナビゲーション機能はprefetchを実行し出力をキャッシュする。もし、dynamic renderingをしたい場合はfetchに{ cache: 'no-store' }をつける。

クライアントコンポーネントではReactのuse hookを使ってStremaするかTanstack Queryのようなサードパーティー製のfetchライブラリを使うとドキュメントにはある。(これclient fetchはもうライブラリの使用が前提ということなのだろうか)

use hookを使うとサーバーからclientにデータをstreamすることが可能。これはデータfetchをするサーバーコンポーネントからクライアントコンポーネントにPromiseでデータをPropsで渡すことで実現できる。データfetchをawaitせずにPromiseのまま渡すのが大事。

clientコンポーネントでは渡ってきたPromiseをuse hookに渡してデータを使うだけでよい。(clientコンポーネントはSuspenseで囲う)

サーバーコンポーネントではasync/awaitが使われていると自動的にdynamic renderingを採用する。これはclientからのリクエストをサーバーが受けデータの取得が完了しレスポンスが返ってくるまでユーザーが待つことを意味する。

sequential data fetch

ツリー状のコンポーネントがそれぞれデータフェッチを行う。重複リクエストは排除されない。

こういうの。これは前段のデータフェッチで取得したデータを用いてさらにデータ取得をおこなうようなケースで起こりやすい。こういった場合はデータがすべて取得できるまでユーザーが待たされてしまうのでSuspenseを使うなどしてStreamにしたほうが良い。

parallel data fetch

並列にデータ取得が開始されるパターン。デフォルトではlayoutとpageは並列にデータ取得が開始される。普通にasync/awaitな処理を直列に書くと当然sequential data fetchになってしまうので、データフェッチの関数を外側で定義してPromise.allなどで並列に実行するようにすると良い。

preloading data

事前に必要なデータをフェッチしておくパターン。これはreactのcache関数と組み合わせて使うことで威力を発揮する。

import { cache } from 'react'
import 'server-only'
import { getItem } from '@/lib/data'
 
export const preload = (id: string) => {
  void getItem(id)
}
 
export const getItem = cache(async (id: string) => {
  // ...
})

このようなユーティリティ関数を定義しておけばpreload関数を呼び出すことで事前にデータを用意しておくことができる。preload関数を呼び出すことでフェッチしたデータをキャッシュすることができるので、実際に使うときにはすでにデータある状態となるためフェッチが不要。

ぱんだぱんだ

updating data(Server Action)

Server FunctionsもしくはServer Actionと呼ばれるもの。Server Actionとはサーバーで実行される非同期関数のことで主に更新処理をする目的で設計されている。

Server Functionsは非同期関数の処理の先頭にuse serverディレクティブを記載するだけでよい。サーバーコンポーネント内でもuse serverディレクティブを使ってインラインにServer Functionsを作ることができる。

client コンポーネント内ではServer Functionsを定義することはできない。ただし、use serverディレクテイブが記載されているファイルで定義されたServer Functionsをimportして使用することができる。Server FunctionsはPropsとして渡して使用することもできる。

Server Functionsが使用されるケースは主に以下の2ケース

  • form
  • clientコンポーネントでのイベントハンドラーやuseEffectなどでの使用

formでの使用

formタグのactionにServer Functionsを渡して使用することができる。Server Functionsには自動的にFormDataが引数として渡されて使用することができる。

'use server'
 
export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const content = formData.get('content')
 
  // Update data
  // Revalidate cache
}

イベントハンドラーでの使用

onClickみたいなイベントハンドラーでServer Functionsは使用できる。

useActionState

Server Functionsの実行中にuseActionStateを使用することでローディングコンポーネントなどを表示したりできる。

revalidatePath, revalidateTag

Server Functionsの実行でキャッシュの更新を行いたいときにrevalidatePathもしくはrevalidateTagが使える。Server Functiolnsはデータ更新を行うので例えばユーザーのリストがあってユーザーを削除するServer Functionsを実行したときにrevalidatePathなどを呼び出すことで最新の表示にすることができる。

ちなみにrevalidatePathとrevalidateTagの違いはrevalidatePathは指定のページのキャッシュを更新し、revalidateTagはfetch関数にtagを付与できるようになっているため(たぶんNext拡張)revalidateTagでタグを指定することで指定されたタグのデータキャッシュを更新することができる。

redirect

また、Server Functions内でデータ更新後にredirect関数を使うことで別ページにリダイレクトすることができる。

cookies関数を使うことでcookieの操作もできるよ

useEffect

useEffect内では主に副作用を伴う処理を記載するため、イベントハンドラの登録や画面の初期表示時に実行したい処理などをかける。Server FunctionsをこのuseEffect内で実行することがもちろんできる。

'use client'
 
import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'
 
export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
  const [isPending, startTransition] = useTransition()
 
  useEffect(() => {
    startTransition(async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    })
  }, [])
 
  // You can use `isPending` to give users feedback
  return <p>Total Views: {views}</p>
}

上記は公式ドキュメント記載のコード例でuseTransitionと組み合わせて使用されている。useTransitionはReact18からあるらしいがReact19で非同期関数を引数に取ることができるようになったそうで、Server Functionsを扱えるようになったらしい。

useTransitionは重いレンダリング処理をバックグランウンドで実行しユーザー操作をブロックしないようにするためのフック。これに関してはReact19の主要な機能っぽいので改めて掘り下げたい

ぱんだぱんだ

Cache

Nextにおけるcacheは主に以下がある。

  • fetch
  • unstable_cache
  • revalidatePath
  • revalidateTag

revalidatePathとrevalidateTagは前述した通りでキャッシュの再取得を明示的におこなう。

fetchはデフォルトではcacheしないが、force-cacheのオプションを指定することでfetchしたデータをキャッシュすることができる。

fetchはcacheしないが、Nextはfetchを含むサーバーコンポーネントをprerenderingしてレンダリングしたHTMLをキャッシュする。レンダリングしたHTMLのキャッシュとfetchのキャッシュ以外にもNextはキャッシュの話がいくつも出てきて混乱しやすい。これがNextの複雑なcache戦略というものだろうか

unstable_cacheはDBアクセスなどの非同期関数をunstable_cacheで囲うことでキャッシュをすることができる。キャッシュの寿命はオプションで指定したり、前述したrevalidateTagなどでキャッシュの更新ができるようにタグ付けをしたりできる。

reactのcache関数との違いが気になったがreactのcacheはそのリクエスト内でのみ有効なキャッシュで重複リクエストを防ぐなどの目的で使用できる。前述の例ではpreload。unstable_cacheはリクエストを跨いだキャッシュなのでキャッシュの寿命などを気にする必要はあるがreactのcacheよりも長くキャッシュを使うことができる。

ぱんだぱんだ

エラーハンドリング(Error Boundary)

Server Functions

Server FunctionsではuseActionStateを用いてエラーハンドリングができる。その場合、Server Functions内でtry-catchしたりErrorをthrowするのは避け、モデリングしたエラーを返す必要がある。

Server Component

Server Component内でデータ取得をおこなう場合、レスポンスに応じてエラーメッセージを表示したりリダイレクトしたりできる。

Error Boundary

appディレクトリ配下に置いたerror.tsファイルはErrorBoundaryコンポーネントとしてpageをラップし、エラー時にfallbackとしてエラーコンポーネントをレンダリングする。Error Boundaryはイベントハンドラー内でのエラーをハンドリングしないため、そのような場合はstateなどで自前でtry-catchして管理する必要がある。

useTransitionのstartTransitionからthrowされたerrorは最も近いError Boundaryで拾われるらしい。

Global errors

global-error.jsといったファイルを配置すればグローバルなエラー画面を配置することができる。これはルートのlayoutとpageを置き換えるらしいのでhtmlタグから書かないといけないらしい。

ぱんだぱんだ

CSS, Image, Font

css

cssに関してはTailwindやCSS Modulesなどが使える。

Image

Nextではimgタグを拡張したImageコンポーネントが使えるのでこれを使うと良い。Imageコンポーネントにはローカルファイル、リモートファイルの両方を指定することができ、ローカルに関してはNextが最適化してくれるのでwidthやheightは指定しなくても自動でいい感じにしてくれるらしい。

リモートファイルを使う場合は、next.config.jsに以下のようにリモートサーバーの情報を記載する必要がある。

import type { NextConfig } from 'next'
 
const config: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        port: '',
        pathname: '/my-bucket/**',
        search: '',
      },
    ],
  },
}
 
export default config

font

フォントはnext/fontパッケージを使ってGoogle fontsなどが使えるよ。

ぱんだぱんだ

Metadata

defaultで以下のmetaタグは付く

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

静的なmetadataは以下のようにlayoutかpageでMetadataオブジェクトをexportすれば良い。

import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'My Blog',
  description: '...',
}
 
export default function Page() {}

fetchしたデータをもとにMetadataを設定するような場合、以下のようなgenerateMetadata()をexportすればよい。

import type { Metadata, ResolvingMetadata } from 'next'
 
type Props = {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
 
export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const slug = (await params).slug
 
  // fetch post information
  const post = await fetch(`https://api.vercel.app/blog/${slug}`).then((res) =>
    res.json()
  )
 
  return {
    title: post.title,
    description: post.description,
  }
}
 
export default function Page({ params, searchParams }: Props) {}

dynamic rendaringでgenerateMetadataを使う場合、generateMetadaの処理が終わるまでブロックされてしまうためstreamでmetadataを注入する。

metadataの生成とレンダリングで同じfetchを必要とする場合があるかもしれないがその場合はreactのcache関数が使える。

favicon, OG画像, robots.txtなどファイルベースで配置することでメタデータを設定することができる。

OG画像に関しては動的に生成することも可能でnext/ogパッケージを使いopengraph-image.tsファイルを作ればよい。

ぱんだぱんだ

Deploy

Vercel一択なのは知ってる。が、みんながVercel以外にデプロイしたいのも知ってる。

公式ドキュメントによるとVercel以外のデプロイでもNextのすべての機能をサポートしているという記載が見られる。

https://nextjs.org/docs/app/getting-started/deploying#docker

docker環境でのデプロイでNextのすべての機能をサポートしているならCloud Runでいいじゃんとも思うけどおそらくNextのキャッシュ機能が完全には機能しない。

ちゃんとした情報見つけられなかったのだけどたぶんCloud Runみたいなサーバーレスコンテナ環境にデプロイする場合以下の記事にあるようなカスタムのCacheハンドラーを実装する必要がある。

https://zenn.dev/akfm/articles/nextjs-cache-handler-redis

Redis環境用意して上記にあるような実装を用意するだけだがVercelならこのようなことは必要ないのかと思うとだるい。そもそも、Vercelなら特に用意する必要のないインフラ環境を自前で用意する必要があり、Cloud Runであればdockerイメージもそうだし、Cloud Run環境、CDNなどをすべて管理する必要がある。これに加えてRedis環境とカスタムのCache handlerも必要なのかと思うとNextから離れたくなる気持ちもわかる。

以下はECSの話だけどself-hostの環境にNextをデプロイする際に考慮しないといけないことは上で書いたこと以上に多そうで、見ただけでやる気はなくなる

https://zenn.dev/snaka/scraps/dbe838e44d1b71

じゃあどうするのがいいかというと以下のような選択肢がある。

  • 黙ってVercel使う
  • Cloud Runのようなself-hostな環境をがんばって構築、運用する
  • Nextの一部機能をあきらめてself-hostする
  • Next使うのをやめる

ここからは個人的な感想だがNextの今の機能と設計思想は素晴らしく、それはReactの設計思想なのかもしれないけどその思想を取り込んだ今のNextはほんとによくできてると思う。なので、他を知らないけどNextを自分なら使いたいなと思う。で、Next使うならその機能は可能な限りすべて使いたい。というか、制限つくならNextじゃなくていいじゃんという気持ちになる。そうなるとVercel使うか頑張るかだけど仕事とかで特にコスト面気にせず使っていいならVercel使うと思うし、そうじゃないならCloud Runとかにがんばってデプロイすると思う。

まあでもやっぱりだるいのでCloudflareのWorkerにデプロイできるのが楽そうだなと思う。もしくは、Next以外で良さそうなフレームワークを知りたい。それでSvelt kitいいぞみたいな話になるんかな。個人的にはVueかNuxtもう一回日の目をみてもいい気がするけど。Next以外でいいならCloudflare Workerもしくはpagesにデプロイで終わりな気がするし。

以下の記事も参考になる

https://laiso.hatenablog.com/entry/2024/10/12/000528

ISRやめるという選択も。やっぱりNextの機能や設計は素晴らしいけどそれに追従するコストみたいなのはあるから(Vercelを使わないならなおさら)Next使わないのもけっこう選択肢としてはありだよなとは思うけど他に良いフレームワーク知らない。

https://zenn.dev/team_zenn/articles/5e9547a5c207e3

一応CludflareでNextを現実的に動かせるようになったよ記事。ただこれってISR的な部分触れられてないけどCustomCacheHandlerの実装とかは必要だよね?

あ、アダプターのplugin内でCloudflare KVをキャッシュ先として使うようにCustomCache Handlerの実装されてるからいらないのか?????ならCloudflareで良くね

https://zenn.dev/mizchi/articles/cloudflare-opennext-prisma-no-rust

今のNextはServerActionsがでたことでフルスタック感が以前にも増したフロントエンドフレームワークになってるからキャッシュの一部機能とか使えなくても十分選択する価値はあるよね、そもそも

Nextの機能を最大限使えるのが理想かもしれないけどNext + Coudflareでサクッとデプロイ&運用コスト削減&API RouteやCloudflare Workers + HonoのAPIをServer Actionsで呼び出すとかにするといい感じのフルスタックNext環境ができそうなのでそれでいいのかもしれない(現状use cacheやPPRのサポートはしていないがそこが無くても十分といえば十分)

ぱんだぱんだ

最後に以下の神zenn本を読む

https://zenn.dev/akfm/books/nextjs-basic-principle

  • データフェッチはサーバーコンポーネントでしよう
  • データフェッチはなるべく末端のコンポーネントでしよう
  • pageコンポーネントでフェッチしてPropsで渡す必要はない
  • これはNextのRequest Memonizationの機能で同一リクエスト内の同じフェッチは一つにまとめられるから
  • 同一リクエストであることが重要なので別途fetcher.tsのような感じで共通化しておくと良い
  • その場合、サーバーコンポーネントでのみ使用する想定なのでserver-onlypackageをimportしておくと良い
  • コンポーネントはデータフェッチ単位で分割すると良い
  • データフェッチが複数ある場合はPromise.allなどを使って並列データフェッチできる
  • 兄弟関係のコンポーネントは並列にレンダリングされる
  • 親子関係の場合でも、Request Memonizationを活用したpreloadを使うことで平行データフェッチを実現することができる
  • preload関数はpreloadなのでawaitしない
  • データフェッチを細かく分割していくとN+1問題が起こりやすくなる
  • これはGraphQLの文脈でも使われるDataLoaderを使うと良い
  • このようにNextは細粒度のAPI設計が推奨されているし相性が良い
  • 従来、通信回線が貧弱なクライアントフェッチでは通信回数がボトルネックになることがあるため一度に全部取ってくるような粗粒度なAPI設計がされたし、細粒度なAPIだったとしても通信回数を減らし必要なデータだけ効率的に取得するためにGraphQLをBFFとして開発する手法が流行った
  • RSCはGraphQLの精神的後継の位置づけとなっており、従来GraphQLがやっていた細粒度のAPIのまとめ役みたいなのはRSCでも可能となっている
  • このようにRSCは貧弱なクライアントフェッチではなく高速なサーバー間通信を使用すること、ページ単位ではなくコンポーネント単位でデータフェッチの設計を可能とする素晴らしい仕組みだ
  • RSCは従来問題となっていたAPIの粒度の問題やclientフェッチの不安定さを解決する仕組みとなっているため、細粒度のAPI設計と相性が当然のことながら良く、GraphQLの後継ともなっている
  • ユーザー操作におけるデータフェッチ(検索条件を入力して一覧をテーブル表示するようなページなど)はuseActionsStateとServer Actionsが使える
  • 具体的にはformのactions属性にuseActionsStateにServerActionsを渡して得られたactionを指定するだけでよい
  • これはReactにおけるトランジションの話とかが裏側にあるのでuhyoさんの記事を読んでおくと良い
  • clientコンポーネントを使う場面は主に以下
    • useStateやイベントハンドラーなどのクライアント処理
    • RSC対応していないサードパーティー性のコンポーネントライブラリの使用
    • RSC payloadの削減
  • Container/Presentialパターン
  • Nextのcacheの仕組みは4つある
    • Request Memonization
    • Data Cache
    • Full Route Cache
    • Router Cache
  • Request Memonizationは前述しているようにリクエスト単位の非常に寿命が短いキャッシュで重複リクエストを排除してくれる
  • Full Route CacheはHTMLやRSC ペイロードのキャッシュで、Static Renderingのレンダリング結果のキャッシュとなる。
  • fetchが取得したデータのキャッシュがData Cacheとなる
  • Static Renderingの場合はFull Route Cacheが効くが、Dynamic Renderingの場合はリクエストのたびにデータフェッチが発生し、高速なサーバー間通信をしているとしてもパフォーマンスに影響が出る
  • そのため、データフェッチを結果をキャッシュしておくのがData Cahceの役割
  • Data Cacheはfetchのオプションにforce-cacheを指定することで有効になり、この場合、Static Renderingになるため従来のSSGに相当する
  • fetchのオプションにno-storeを指定するとDynamic Renderingになるため、リクエストのたびにデータフェッチがはしる。
  • オプションを指定しない場合、デフォルトではData CahceされないでStatic Renderingになる(動的APIがなければ)
  • ここけっこうややこしい
  • Data CacheはrevalidateTagsなどでServer Actionsのあとなどに更新することが可能
  • 最後にRouter Cache
  • これはいわゆるclient cacheで、prefetchやnavigationなどで取得したRSC payloadをキャッシュする。
  • キャッシュはclient側で保持されるので全ユーザーのRouter Cacheをリセットするみたいなことは当然できない
  • Router Cacheは中の実装がけっこう複雑っぽく完全に挙動を理解するのは難しい印象だが、執筆時点でstatic renderingは5min、dynamic renderingは0minがデフォルトのキャッシュ保持期間だということだけでも覚えておけば良さそう
  • DynamicIOとuseCache
ぱんだぱんだ

Nextを理解する上で理解しておいたほうが良いReactの仕様

主にuhyoさんの記事

use hookについて

https://zenn.dev/uhyo/articles/react-use-rfc

https://zenn.dev/uhyo/articles/react-use-rfc-2

Promiseを解決するためのhook。PromiseをプリミティブなデータとみなすことでReactコンポーネント内での使用を許容しているみたいな話。

RSCについて

https://zenn.dev/uhyo/articles/react-server-components-multi-stage

https://zenn.dev/uhyo/books/rsc-without-nextjs

ちょっと、zenn本の方は内容難しかったけど雰囲気は掴めた。RSCを多段計算とみなすことで理解しやすくなるよという話でサーバーコンポーネントの実行をstage0、クライアントコンポーネントの実行をstage1として2段階の処理としてRSCを考えるといいよという話。

従来のSSRとサーバーコンポーネントは混在しやすいが従来のSSRはサーバー側でstage1を実行するしクライアント側でもstage1の実行をするらしい。これはサーバー側でHTMLを完全にレンダリングして一旦返すが、クライアント側でもサーバー側で実行した内容をそのまま実行しHTMLを置き換える。これをハイドレーションと呼ぶらしく、従来型のSSRがこのような処理になっているは知らなかった。完全にサーバー側でHTMLをレンダリングしてclientに返すだけかと思ってた。

ハイドレーションについて

https://zenn.dev/dozo13189/articles/07e96c182afa46

でもよく考えたらRSCにおけるclientコンポーネントに相当するような状態を持ってたりイベントハンドラーを扱ったりみたいなコンポーネントを含んでいる場合、完全にサーバー側でHTMLをレンダリングしきることってできないよね。だからクライント側での処理の実行とハイドレーションが必要だったのか。納得

RSCではサーバーコンポーネントが実行されると以下のようなレンダリング結果が得られる。uhyoさんのzenn本から持ってきた。

M1:{"id":"__mod__","name":"Clock","chunks":[]}
J0:["$","div",null,{"children":[["$","h1",null,{"children":"React Server Components example"}],[["$","p",null,{"children":"Hello, world!"}],["$","@1",null,{}]]]}]

これはサーバーコンポーネントの情報は一切持っておらずサーバーコンポーネントだった部分は完全にHTMLとして表現されているためサーバーコンポーネントはレンダリングされているそうな。

これがclient側でHTMLとしてレンダリングされ、stage1と呼ばれている部分の処理。上記の例だとクライアントコンポーネントを含んでいるがクライアントコンポーネントの部分はコンポーネントの情報を含むような形でレンダリングされる。

RSC時代のSSRはサーバー側でstage0とstage1が実行されclientコンポーネント以外の部分は完全にHTMLとしてレンダリングされclientコンポーネント部分はstage0までレンダリングされてclient側でstage1が実行されるみたいな感じと理解

Suspenseについて

https://zenn.dev/uhyo/books/react-concurrent-handson

Suspenseがでたことで、コンポーネントのレンダリングをサスペンドするということができるようになった。サスペンドはSuspense内からPromiseがthrowされることで発生し、コンポーネントがサスペンドしている間はフォールバックに設定したコンポーネントが表示される。

これはfetch中のloadingを表示したりがシンプルな使い方だが、非同期関数を扱うコンポーネントが扱いやすくなったと理解した。

https://zenn.dev/uhyo/books/react-concurrent-handson-2

トランジションの話。サスペンド中に発生するstateの更新みたいなときにこの内容を理解しておくと良さそう。

React19について

https://zenn.dev/uhyo/books/react-19-new

actionが一番大事そう。Suspenseの話はどちらかと言えばデータfetchのときの話でアクションは更新のときの話。formとか。で、formにactionという属性が指定できるようになって、ここに非同期関数をactionとして渡せるようになった。アクションによる非同期関数の実行によってトランジションの話やErrorBoundaryの話が出てくることになる。

改めてuhyoさんってすごいな

ぱんだぱんだ

Nuxt

Next学んでからNuxtの機能見てみるとNuxtがEasyって言われるのがすごくわかるな。

https://zenn.dev/ytr0903/articles/197f5386ca4309

Vue2時代とかは知らないけど今のVueやNuxtはめちゃくちゃ開発体験良さそう。というか、Vue3のComposition APIがかなり体験良いのだと思う。流行ってくれればいいのに、、