Menu

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

14から15へのアップグレード

Next.jsバージョン15に更新するには、upgrade codemods を使用できます。

Terminal
npx @next/codemod@canary upgrade latest

手動で行う場合は、最新のNextおよびReactバージョンをインストールしていることを確認してください。

Terminal
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も最新バージョンにアップグレードしてください。

非同期Request API(破壊的変更)

以前は同期的だった、ランタイム情報に依存する動的APIが非同期になりました。

移行の負担を軽減するために、codemodを利用してプロセスを自動化でき、APIは一時的に同期的にアクセスできます。

cookies

推奨される非同期の使用方法

import { cookies } from 'next/headers'
 
// 変更前
const cookieStore = cookies()
const token = cookieStore.get('token')
 
// 変更後
const cookieStore = await cookies()
const token = cookieStore.get('token')

一時的な同期の使用方法

app/page.tsx
TypeScript
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')

一時的な同期の使用方法

app/page.tsx
TypeScript
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()

一時的な同期の使用方法

app/page.tsx
TypeScript
import { draftMode, type UnsafeUnwrappedDraftMode } from 'next/headers'
 
// 変更前
const { isEnabled } = draftMode()
 
// 変更後
// 開発環境で警告が出力されます
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode

paramssearchParams

非同期Layout

app/layout.tsx
TypeScript
// 変更前
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
}

同期Layout

app/layout.tsx
TypeScript
// 変更前
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
}

非同期Page

app/page.tsx
TypeScript
// 変更前
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
}

同期Page

'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
}
 

Route Handlers

app/api/route.ts
TypeScript
// 変更前
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
}

runtime設定(破壊的変更)

runtimeセグメント設定は以前、edgeに加えてexperimental-edgeの値をサポートしていました。両方の設定は同じものを参照していたため、オプションを簡素化するためにexperimental-edgeを使用した場合はエラーが発生するようになります。これを修正するには、runtime設定をedgeに更新してください。これを自動的に行うcodemodがあります。

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' }) // キャッシュあり
 
  // ...
}

layoutまたはpage内のすべてのfetchリクエストをキャッシングにオプトインするには、export const fetchCache = 'default-cache'セグメント設定オプションを使用します。個々のfetchリクエストがcacheオプションを指定している場合は、そちらが使用されます。

app/layout.js
// これはrootlayoutなので、アプリ内のすべての
// 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' }) // キャッシュなし
 
  // ...
}

Route Handlers

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

app/api/route.js
export const dynamic = 'force-static'
 
export async function GET() {}

クライアント側Router Cache

<Link>またはuseRouter経由でページ間をナビゲートする場合、pageセグメントはクライアント側router cacheから再利用されなくなります。ただし、ブラウザの前後へのナビゲーションと共有layoutでは引き続き再利用されます。

pageセグメントをキャッシングにオプトインするには、staleTimes設定オプションを使用できます。

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

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

next/font

@next/fontパッケージは、組み込みのnext/fontに有利な形で削除されました。インポートを安全かつ自動的に名前変更するためにcodemodがあります。

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

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

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

NextRequestの地理的位置情報

NextRequestgeoプロパティとipプロパティはホスティングプロバイダーが提供する値であるため削除されました。この移行を自動化するためにcodemodがあります。

Vercelを使用している場合は、代わりに@vercel/functionsgeolocation関数とipAddress関数を使用できます。

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)
 
  // ...
}