1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js 14→16移行で実際につまずいた6箇所(codemod・proxy・ESLint flat config)

1
Posted at

個人開発でチャートリプレイ型のトレード練習サイト(kabu-renshu.com)をNext.js 14で作っていましたが、npm audit に残っていたNext.js 14系のhigh脆弱性(修正版が16系のみでバックポートなし)を根治するため、2026年9月にNext.js 16へ移行しました。公式の upgrade codemodを使って14→16へ一気に上げる方針を取りましたが、codemod任せでは終わらない箇所がいくつかあったので、実際に何を直したかを記録します。

環境

項目 移行前 移行後
next 14.2.35 16.3.4
react / react-dom 18 19.2.8
eslint / eslint-config-next 8 / 14.2.35 9.39.5 / 16.3.4
typescript 5系 変更なし

やったこと一覧

# 症状 対処
1 codemodがページに挿入したexport const instant = falseでビルドエラー cacheComponents未使用のため除去
2 experimental.outputFileTracingIncludesにビルド警告 トップレベルのoutputFileTracingIncludesへ移動
3 middleware.tsが非推奨に src/proxy.tsへリネームしexport function proxyに
4 動的ルートのparamsが同期参照のまま next-async-request-api codemod+手動でawait params化
5 OGP画像ルートのparams未対応・runtime='edge'が不要に opengraph-image.tsxをawait params化、export const runtime = 'edge'を削除
6 next lint廃止でlintコマンドが動かない .eslintrc.jsonをeslint.config.mjs(flat config)へ移行

以下、それぞれの中身です。

1. codemodがページに入れたinstant = falseでビルドが落ちた

npx @next/codemod@canary upgrade latest を対話的に進めると、途中でcache-components-instant-falseという別のcodemodの実行を勧められます。これはcacheComponentsを有効化する準備として、app配下のpage/layoutファイルにexport const instant = falseを挿入するものです。

このアプリはcacheComponentsを使っていません。その状態でinstantが付いたページがあるとnext buildが「instantはcacheComponents有効時のみ使える」趣旨のエラーで止まりました。対処は挿入された行を全て取り除くだけです。コミットにはその旨だけ残しています。

chore(next16): 依存更新とcodemod適用(next 16.3.4 / react 19.2 / proxy化 / async params)

- npx @next/codemod@canary upgrade latest の成果物。codemodが付与した instant=false は
  cacheComponents無効のためビルドエラーになるので除去

除去してからコミットしたので、リポジトリの差分にはinstantの痕跡が残っていません。教訓は「upgrade codemodが勧めてくる追加codemodは、自分のプロジェクトの前提(ここではcacheComponentsを使うかどうか)を確認してから受けるかどうか決める」で、codemodの出力をgit diff --statで確認してから1コミットにまとめる運用にしていたおかげで検出できました。

2. outputFileTracingIncludesの配置

/practice/[id]は動的ルートですが、記事タイトルの読み取りにcontent/blog配下のファイルをサーバーレス関数へ同梱する必要があり、これをexperimental.outputFileTracingIncludesで指定していました。16でビルドすると「トップレベルへ移すように」という警告が出たので、それに従って移動しました(どのバージョンで昇格したかは公式ガイドで確認できていません)。

 const nextConfig = {
-  experimental: {
-    outputFileTracingIncludes: {
-      '/practice/[id]': ['./content/blog/**'],
-    },
+  outputFileTracingIncludes: {
+    '/practice/[id]': ['./content/blog/**'],
   },

ビルド警告に従って移すだけですが、/practice/[id]で関連記事タイトルが空になっていないかをPreview環境で目視確認しています。

3. middleware.ts→proxy.ts

Next.js 16でmiddlewareは非推奨になり、ファイル名proxy.ts・export名proxyが新しい形になりました。このアプリのmiddlewareはUser-Agent判定によるボットブロックのみで、edge固有のAPIは使っていなかったため、単純なリネームで済みました。

-import { type NextRequest, NextResponse } from 'next/server';
-import { isBlockedUserAgent } from '@/lib/bot-block';
-
-export function middleware(request: NextRequest): NextResponse {
+export function proxy(request: NextRequest): NextResponse {
   if (isBlockedUserAgent(request.headers.get('user-agent'))) {
     return new NextResponse('Forbidden', { status: 403 });
   }

proxyはNode.jsランタイム固定になる仕様変更も伴いますが、もともとUser-Agent判定だけの軽量な処理だったため、挙動面での影響は確認していません。

4. 動的ルートのparamsが非同期化

Next.js 15〜16でapp routerのparams/searchParamsはPromiseになりました。npx @next/codemod@canary next-async-request-api . で機械変換したうえで、/api/scenarios/[id]/route.tsやblog/[slug]/page.tsxなど6ファイル前後を手直ししています。

 interface Props {
-  params: { slug: string };
+  params: Promise<{ slug: string }>;
 }

-export async function generateMetadata({ params }: Props): Promise<Metadata> {
+export async function generateMetadata(props: Props): Promise<Metadata> {
+  const params = await props.params;
   const post = getPostBySlug(params.slug);

Route Handlerも同様です。

-export async function GET(
-  _request: NextRequest,
-  { params }: { params: { id: string } }
-) {
+export async function GET(_request: NextRequest, props: { params: Promise<{ id: string }> }) {
+  const params = await props.params;
   const { id } = params;

cookies()/headers()の直接呼び出しはこのアプリには無かったため、対応箇所はparamsのPromise化のみでした。

5. OGP画像ルートのparamsとedge runtime

opengraph-image.tsxもPageコンポーネントと同様にparamsがPromise化されており、next-async-request-apiのcodemod対象からは漏れていたため手動で修正しました。

 interface Props {
-  params: { slug: string };
+  params: Promise<{ slug: string }>;
 }

-export default async function Image({ params }: Props) {
+export default async function Image(props: Props) {
+  const params = await props.params;
   const post = BLOG_META[params.slug];

あわせて、トップページのOGP画像ルート(paramsなし)に付けていたexport const runtime = 'edge'は不要になったため削除しています。

 import { ImageResponse } from 'next/og';
 
-export const runtime = 'edge';
 export const alt = '株の練習シミュレーター|無料チャートリプレイで株トレード練習';

このアプリのOGP画像ルートはgenerateStaticParamsを付けるとビルド時プリレンダでnext/ogがInvalid URLエラーを起こすため、あえてリクエスト時生成にしている経緯があり、ローカル(Windows)環境では検証できず、Vercel Previewで初めて実機確認できる箇所でした。

6. next lint廃止とESLint flat configへの移行

Next.js 16でnext lintコマンドが廃止され、ESLintの実行はプロジェクト側の責任になりました。.eslintrc.jsonをeslint.config.mjsのflat configへ移行し、lintスクリプトをeslint .に変更しています。

import { defineConfig } from "eslint/config";
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";

export default defineConfig([
  {
    ignores: [".next/**", "node_modules/**", "seed/**", "_old/**", "docs/**", "public/**"],
  },
  {
    extends: [...nextCoreWebVitals, ...nextTypescript],
    rules: {
      "react-hooks/refs": "off",
      "react-hooks/set-state-in-effect": "off",
      "react-hooks/purity": "off",
      "react-hooks/immutability": "off",
      "react-hooks/preserve-manual-memoization": "off",
    },
  },
]);

next buildはもうlintを実行しないため、作業完了時の手順としてnpm run lintを別途手で回す運用に切り替えています。

React Compiler系の新設ルール30箇所は「直さない」と判断した

eslint-config-next 16に同梱されるeslint-plugin-react-hooks v7で、React Compilerを前提にした新しいルール(react-hooks/refs、set-state-in-effectなど)が有効になり、既存コードで30箇所が該当しました。この5ルールは上記の通りoffにしていますが、機械的に無効化しただけでなく、1件ずつ内容を精査したうえでの判断です。

主な内訳は次の通りです(残りはpurity・immutability・preserve-manual-memoizationが各1〜2件)。

  • effect内でのsetState(10箇所): useSearchParamsを使うとページ全体が動的化してSSGが崩れるため、あえてuseEffectでURLパラメータやlocalStorageの値を読んでsetStateしている箇所。遅延初期化に書き換えるとSSR時にwindowが無くhydration mismatchを起こすため、意図的な実装として現状維持
  • render中のref同期(12箇所): 主に再生タイマーやチャート描画コールバックが「render後のeffectを待たずに最新値を要求する」ため、render本体でxxxRef.current = xxxを同期している箇所。useEffect化すると1フレームのタイミングずれが生まれ、約定価格やタイマー処理に影響しうるためリスクが高いと判断
  • 安全に直せる3箇所: モーダルのEscapeキー処理用ref同期(OrderDialog.tsx、ConfirmModal.tsx、FeedbackModal.tsx)はuseEffect化しても影響が限定的だが、React Compiler自体を有効化していないため直しても実行時の恩恵はなく、優先度は低いと結論

React Compilerの本体(reactCompiler: true)についても、PracticeClient.tsxが既にuseCallback/useMemo/refによる手動最適化を広範に施した設計であるため、有効化のメリットは薄く、上記のref依存設計を見直す別タスクとして切り出さない限り着手しない、という判断にしています。

まとめ

Next.js 14→16の移行は、公式のupgrade codemodとnext-async-request-api codemodでほとんどの機械的な変換はカバーされましたが、そのまま鵜呑みにできたわけではありませんでした。

  • upgrade codemodが勧める追加codemod(cache-components-instant-false)は、プロジェクトの前提(cacheComponents未使用)と噛み合わないとビルドを壊す
  • experimentalから昇格した設定はビルド警告を読んで手で移す必要がある
  • OGP画像ルートのようにcodemodの対象から漏れる箇所がある
  • next typegen(next buildでも走る)がtsconfig.jsonを書き換える(targetがES2017になり、.next/typesのincludeが足される)。差分に驚かないよう、これは意図した変更としてそのままコミットした
  • next lint廃止に伴うESLint flat config移行では、新設ルールをただoffにするのではなく、なぜそのコードがそう書かれているか(hydration mismatch回避、タイマー処理のタイミング要件など)を1件ずつ確認してから判断した

「codemodを流して終わり」ではなく、差分を1つずつレビューし、プロジェクト固有の設計判断(SSG維持のためのeffect初期化、タイマー精度のためのref同期)と新しいルールがぶつかる箇所を見極める作業が、実質的な移行コストの大半でした。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?