バージョン15へのアップグレード方法

バージョン14から15へのアップグレード

Next.jsバージョン15に更新するには、upgradeコードモッドを使用できます:

ターミナル
npx @next/codemod@canary upgrade latest

手動で行う場合は、最新のNext.jsとReactバージョンをインストールしてください:

ターミナル
npm i next@latest react@latest react-dom@latest eslint-config-next@latest

知っておくと良いこと:

  • ピア依存関係の警告が表示された場合、reactreact-domを推奨バージョンに更新するか、--forceまたは--legacy-peer-depsフラグを使用して警告を無視できます。Next.js 15とReact 19が安定版になればこの作業は不要になります。

React 19

  • reactreact-domの最小バージョンが19になりました。
  • useFormStateuseActionStateに置き換えられました。useFormStateフックはReact 19でも利用可能ですが非推奨で、将来のリリースで削除されます。useActionStateが推奨され、pending状態を直接読み取るなどの追加プロパティが含まれています。詳細はこちら
  • useFormStatusにはdatamethodactionなどの追加キーが含まれるようになりました。React 19を使用していない場合、pendingキーのみ利用可能です。詳細はこちら
  • React 19アップグレードガイドで詳細を確認してください。

知っておくと良いこと: TypeScriptを使用している場合、@types/react@types/react-domも最新バージョンにアップグレードしてください。

非同期リクエストAPI(破壊的変更)

以前は同期処理だったランタイム情報に依存するDynamic APIが非同期になりました:

移行負担を軽減するため、コードモッドが利用可能で、一時的にAPIを同期的にアクセスすることもできます。

cookies

推奨される非同期使用法

import { cookies } from 'next/headers'

// 以前
const cookieStore = cookies()
const token = cookieStore.get('token')

// 以降
const cookieStore = await cookies()
const token = cookieStore.get('token')

一時的な同期使用法

import { cookies, type UnsafeUnwrappedCookies } from 'next/headers'

// 以前
const cookieStore = cookies()
const token = cookieStore.get('token')

// 以降
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
// 開発時には警告が表示されます
const token = cookieStore.get('token')

headers

推奨される非同期使用法

import { headers } from 'next/headers'

// 以前
const headersList = headers()
const userAgent = headersList.get('user-agent')

// 以降
const headersList = await headers()
const userAgent = headersList.get('user-agent')

一時的な同期使用法

import { headers, type UnsafeUnwrappedHeaders } from 'next/headers'

// 以前
const headersList = headers()
const userAgent = headersList.get('user-agent')

// 以降
const headersList = headers() as unknown as UnsafeUnwrappedHeaders
// 開発時には警告が表示されます
const userAgent = headersList.get('user-agent')

draftMode

推奨される非同期使用法

import { draftMode } from 'next/headers'

// 以前
const { isEnabled } = draftMode()

// 以降
const { isEnabled } = await draftMode()

一時的な同期使用法

import { draftMode, type UnsafeUnwrappedDraftMode } from 'next/headers'

// 以前
const { isEnabled } = draftMode()

// 以降
// 開発時には警告が表示されます
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode

params & searchParams

非同期レイアウト

// 以前
type Params = { slug: string }

export function generateMetadata({ params }: { params: Params }) {
  const { slug } = params
}

export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}

// 以降
type Params = Promise<{ slug: string }>

export async function generateMetadata({ params }: { params: Params }) {
  const { slug } = await params
}

export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = await params
}

同期レイアウト

// 以前
type Params = { slug: string }

export default function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}

// 以降
import { use } from 'react'

type Params = Promise<{ slug: string }>

export default function Layout(props: {
  children: React.ReactNode
  params: Params
}) {
  const params = use(props.params)
  const slug = params.slug
}

非同期ページ

// 以前
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }

export function generateMetadata({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}

export default async function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}

// 以降
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

export async function generateMetadata(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}

export default async function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}

同期ページ

'use client'

// 以前
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }

export default function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}

// 以降
import { use } from 'react'

type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

export default function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}
// 以前
export default function Page({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}

// 以降
import { use } from "react"

export default function Page(props) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}

ルートハンドラー

app/api/route.ts
// 以前
type Params = { slug: string }

export async function GET(request: Request, segmentData: { params: Params }) {
  const params = segmentData.params
  const slug = params.slug
}

// 以降
type Params = Promise<{ slug: string }>

export async function GET(request: Request, segmentData: { params: Params }) {
  const params = await segmentData.params
  const slug = params.slug
}
app/api/route.js
// 以前
export async function GET(request, segmentData) {
  const params = segmentData.params
  const slug = params.slug
}

// 以降
export async function GET(request, segmentData) {
  const params = await segmentData.params
  const slug = params.slug
}

runtime設定(破壊的変更)

runtimeセグメント設定では、以前はedgeに加えてexperimental-edge値がサポートされていました。両方の設定は同じものを指しており、オプションを簡素化するため、experimental-edgeを使用するとエラーになります。修正するには、runtime設定をedgeに更新してください。コードモッドが自動的に行うことができます。

fetchリクエスト

fetchリクエストはデフォルトでキャッシュされなくなりました。

特定のfetchリクエストをキャッシュに含めるには、cache: 'force-cache'オプションを渡します。

app/layout.js
export default async function RootLayout() {
  const a = await fetch('https://...') // キャッシュされない
  const b = await fetch('https://...', { cache: 'force-cache' }) // キャッシュされる

  // ...
}

レイアウトまたはページ内のすべてのfetchリクエストをキャッシュに含めるには、export const fetchCache = 'default-cache'セグメント設定オプションを使用できます。個々のfetchリクエストがcacheオプションを指定している場合、それが優先されます。

app/layout.js
// これはルートレイアウトなので、アプリ内のすべてのfetchリクエストで
// 独自のcacheオプションが設定されていないものはキャッシュされます。
export const fetchCache = 'default-cache'

export default async function RootLayout() {
  const a = await fetch('https://...') // キャッシュされる
  const b = await fetch('https://...', { cache: 'no-store' }) // キャッシュされない

  // ...
}

ルートハンドラー

ルートハンドラーGET関数はデフォルトでキャッシュされなくなりました。GETメソッドをキャッシュに含めるには、ルートハンドラーファイルでexport const dynamic = 'force-static'などのルート設定オプションを使用できます。

app/api/route.js
export const dynamic = 'force-static'

export async function GET() {}

クライアントサイドルーターキャッシュ

<Link>またはuseRouterを介してページ間を移動する場合、ページセグメントはクライアントサイドルーターキャッシュから再利用されなくなりました。ただし、ブラウザの戻る/進むナビゲーション時や共有レイアウトでは引き続き再利用されます。

ページセグメントをキャッシュに含めるには、staleTimes設定オプションを使用できます:

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

module.exports = nextConfig

レイアウトローディング状態は引き続きキャッシュされ、ナビゲーション時に再利用されます。

next/font

@next/fontパッケージは組み込みのnext/fontに置き換えられました。コードモッドを使用してインポートを安全かつ自動的にリネームできます。

app/layout.js
// 以前
import { Inter } from '@next/font/google'

// 以降
import { Inter } from 'next/font/google'

bundlePagesRouterDependencies

experimental.bundlePagesExternalsが安定版になり、bundlePagesRouterDependenciesに名称変更されました。

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 以前
  experimental: {
    bundlePagesExternals: true,
  },

  // 以降
  bundlePagesRouterDependencies: true,
}

module.exports = nextConfig

serverExternalPackages

experimental.serverComponentsExternalPackagesが安定版になり、serverExternalPackagesに名称変更されました。

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 以前
  experimental: {
    serverComponentsExternalPackages: ['package-name'],
  },

  // 以降
  serverExternalPackages: ['package-name'],
}

module.exports = nextConfig

Speed Insights

Next.js 15でSpeed Insightsの自動計測が削除されました。

Speed Insightsを引き続き使用するには、Vercel Speed Insightsクイックスタートガイドに従ってください。

NextRequest のジオロケーション (Geolocation)

NextRequestgeo および ip プロパティは削除されました。これらの値はホスティングプロバイダーによって提供されるためです。この移行を自動化するための codemod が利用可能です。

Vercel を使用している場合、代わりに @vercel/functions から geolocationipAddress 関数を使用できます:

middleware.ts
import { geolocation } from '@vercel/functions'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const { city } = geolocation(request)

  // ...
}
middleware.ts
import { ipAddress } from '@vercel/functions'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const ip = ipAddress(request)

  // ...
}