kt-tech.blog

画像

【実践】Next.js 13→16メジャーアップグレードの全記録 — 破壊的変更と対応策

Share
📌
概要: Next.js 13.4から16.1へのメジャーアップグレードで遭遇した破壊的変更と対応策をまとめました。params型のPromise化、useSearchParamsのSuspense必須化、Turbopack標準化など、各バージョンの重要な変更点を解説します。

[toc]

はじめに

  • Next.js 13から16への3世代スキップアップグレードを実施した

  • 背景: Cloudflare Workers移行に伴い、OpenNextが要求するNext.js 15以上への対応が必要になった

  • 段階的にやるか一気にやるか検討した結果、一気にアップグレードする方針を選択

  • 各バージョンの破壊的変更を事前に洗い出し、計画的に対応した

この記事でわかること

  • Next.js 14: params型のPromise化への対応方法

  • Next.js 15: useSearchParamsのSuspense境界必須化への対応

  • Next.js 16: Turbopack標準化によるビルド速度向上

  • React 18→19の型定義更新(@types/react@19)

対象読者

  • Next.jsのメジャーアップグレードを予定している方

  • Next.js 13以前のプロジェクトを最新版に追従させたい方

  • 破壊的変更の影響範囲を事前に把握したい方

1. Next.js 14: params型がPromiseに変更

変更内容

Next.js 14以降、動的ルートのparamsがPromiseを返すように変更された。これはApp Routerの非同期レンダリング最適化の一環。

Before(Next.js 13)

// src/app/blogs/[blogId]/page.tsx
export default function BlogDetail({ params }: { params: { blogId: string } }) {
  const { blogId } = params; // 同期的にアクセス可能
  // ...
}

After(Next.js 14+)

// src/app/blogs/[blogId]/page.tsx
export default async function BlogDetail({ params }: { params: Promise<{ blogId: string }> }) {
  const { blogId } = await params; // awaitが必須
  // ...
}

影響範囲

  • [blogId]/page.tsx — ブログ詳細ページ

  • [categoryId]/page.tsx — カテゴリ別一覧ページ

  • [tagId]/page.tsx — タグ別一覧ページ

  • page/[pageId]/page.tsx — ページネーション

  • generateMetadata関数のparams引数も同様に変更

対応のポイント

// generateMetadataも同様にawaitが必要
export async function generateMetadata({ params }: { params: Promise<{ blogId: string }> }) {
  const { blogId } = await params;
  const blog = await getDetail(blogId);
  return { title: blog.title };
}

2. Next.js 15: useSearchParamsにSuspense境界必須

変更内容

Next.js 15では、useSearchParams()を使用するコンポーネントを<Suspense>で囲むことが必須になった。囲まないとビルドエラーが発生する。

問題が発生したコンポーネント

Google Analyticsのページビュー追跡でuseSearchParamsを使用していたため、ビルドが失敗した。

Before

// libs/gtag.ts のPageviewコンポーネント
'use client';
import { useSearchParams } from 'next/navigation';

export function GoogleAnalytics() {
  const searchParams = useSearchParams(); // Suspenseなしで使用
  // ...
}

After

// layout.tsx
import { Suspense } from 'react';
import { GoogleAnalytics } from '@/libs/gtag';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Suspense fallback={null}>
          <GoogleAnalytics />
        </Suspense>
        {children}
      </body>
    </html>
  );
}

検索ページでの対応

検索フォームコンポーネントも同様にSuspenseで囲む必要があった。

<Suspense fallback={<SearchFormSkeleton />}>
  <SearchForm />
</Suspense>

3. Next.js 16: Turbopack標準化

変更内容

Next.js 16ではTurbopackがデフォルトのバンドラーとして標準化された。

ビルド速度の改善

指標 webpack(Next.js 13) Turbopack(Next.js 16)
dev起動 約3.5秒 約1.2秒
HMR 約800ms 約200ms
プロダクションビルド 約45秒 約30秒

注意点

  • next.config.jsのwebpack設定はTurbopackでは無視される

  • カスタムwebpackプラグインを使用している場合は移行が必要

  • 今回のプロジェクトではwebpackのカスタム設定がなかったため影響なし

4. React 18→19の変更点

型定義の更新

React 19では型定義が大幅に変更された。@types/react@19への更新が必要。

npm install @types/react@19 @types/react-dom@19

主な型の変更

// React.FC の children が暗黙的に含まれなくなった
// Before
const Component: React.FC = ({ children }) => { ... };

// After - 明示的にchildrenを定義
const Component: React.FC<{ children: React.ReactNode }> = ({ children }) => { ... };

useRef の変更

// Before: initialValueがnullでもOK
const ref = useRef<HTMLDivElement>(null);

// After: React 19では RefObject の型が変更
// null初期値の場合は MutableRefObject ではなく RefObject を返す
const ref = useRef<HTMLDivElement>(null);
// 型は RefObject<HTMLDivElement | null> になる

Tips

📌
Tip 1: 段階的アップグレード(13→14→15→16)ではなく一気にやった方が楽な場合もある。中間バージョンの動作確認を省略できるため、トータルの作業時間が短くなる
📌
Tip 2: npx @next/codemod@latest upgrade を使うと、破壊的変更の一部を自動で修正してくれる
📌
Tip 3: TypeScriptのstrictモードを有効にしておくと、params型の変更を確実に検出できる
📌
Tip 4: アップグレード前にnpm outdatedで全依存関係の状態を確認し、Next.jsと相性の悪いパッケージを事前に特定しておく

まとめ

  • Next.js 13→16の3世代スキップアップグレードは計画的に行えば十分実用的

  • 最大の破壊的変更はparams型のPromise化(Next.js 14)で、全動的ルートの修正が必要

  • useSearchParamsのSuspense必須化(Next.js 15)はGoogle Analytics等の共通コンポーネントに影響

  • Turbopack標準化(Next.js 16)は基本的にゼロコストで恩恵を受けられる

  • React 19の型変更は@types/react@19への更新で対応可能