跳到主要內容

版本 15

從 14 升級到 15

若要更新到 Next.js 版本 15,你可以使用 upgrade codemod

終端機
npx @next/codemod@canary upgrade latest

如果你偏好手動操作,請確保你安裝的是最新的 Next & React 版本

終端機
npm i next@latest react@latest react-dom@latest eslint-config-next@latest

须知

  • 如果你看到 peer dependencies 警告,你可能需要將 reactreact-dom 更新到建議的版本,或者你使用 --force--legacy-peer-deps 標記來忽略警告。一旦 Next.js 15 和 React 19 都穩定後,這將不再是必要的。

React 19

  • 現在 reactreact-dom 的最低版本為 19。
  • useFormState 已被 useActionState 取代。useFormState Hook 在 React 19 中仍然可用,但它已被棄用,並將在未來的版本中移除。建議使用 useActionState,它包含額外的屬性,例如直接讀取 pending 狀態。了解更多資訊
  • useFormStatus 現在包含額外的鍵,如 datamethodaction。如果你未使用 React 19,則僅 pending 鍵可用。了解更多資訊
  • React 19 升級指南中閱讀更多資訊。

须知: 如果你正在使用 TypeScript,請確保你也將 @types/react@types/react-dom 升級到最新版本。

非同步請求 API (重大變更)

先前依賴執行階段資訊的同步動態 API 現在是非同步的

為了減輕遷移的負擔,codemod 可用來自動化此流程,並且可以暫時同步存取 API。

cookies

import { cookies } from 'next/headers'
 
// Before
const cookieStore = cookies()
const token = cookieStore.get('token')
 
// After
const cookieStore = await cookies()
const token = cookieStore.get('token')

暫時的同步用法

app/page.tsx
import { cookies, type UnsafeUnwrappedCookies } from 'next/headers'
 
// Before
const cookieStore = cookies()
const token = cookieStore.get('token')
 
// After
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
// will log a warning in dev
const token = cookieStore.get('token')

headers

import { headers } from 'next/headers'
 
// Before
const headersList = headers()
const userAgent = headersList.get('user-agent')
 
// After
const headersList = await headers()
const userAgent = headersList.get('user-agent')

暫時的同步用法

app/page.tsx
import { headers, type UnsafeUnwrappedHeaders } from 'next/headers'
 
// Before
const headersList = headers()
const userAgent = headersList.get('user-agent')
 
// After
const headersList = headers() as unknown as UnsafeUnwrappedHeaders
// will log a warning in dev
const userAgent = headersList.get('user-agent')

draftMode

import { draftMode } from 'next/headers'
 
// Before
const { isEnabled } = draftMode()
 
// After
const { isEnabled } = await draftMode()

暫時的同步用法

app/page.tsx
import { draftMode, type UnsafeUnwrappedDraftMode } from 'next/headers'
 
// Before
const { isEnabled } = draftMode()
 
// After
// will log a warning in dev
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode

params & searchParams

非同步版面配置

app/layout.tsx
// Before
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
}
 
// After
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
}

同步版面配置

app/layout.tsx
// Before
type Params = { slug: string }
 
export default function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}
 
// After
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
}

非同步頁面

app/page.tsx
// Before
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
}
 
// After
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'
 
// Before
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
}
 
// After
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
}
// Before
export default function Page({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}
 
// After
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
// Before
type Params = { slug: string }
 
export async function GET(request: Request, segmentData: { params: Params }) {
  const params = segmentData.params
  const slug = params.slug
}
 
// After
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
// Before
export async function GET(request, segmentData) {
  const params = segmentData.params
  const slug = params.slug
}
 
// After
export async function GET(request, segmentData) {
  const params = await segmentData.params
  const slug = params.slug
}

runtime 設定 (重大變更)

除了 edge 之外,runtime 區段設定先前也支援 experimental-edge 值。這兩個設定都指向同一件事,為了簡化選項,如果使用 experimental-edge,現在會產生錯誤。若要修正此問題,請將你的 runtime 設定更新為 edgecodemod 可自動執行此操作。

fetch 請求

fetch 請求不再預設快取。

若要選擇將特定的 fetch 請求加入快取,你可以傳遞 cache: 'force-cache' 選項。

app/layout.js
export default async function RootLayout() {
  const a = await fetch('https://...') // Not Cached
  const b = await fetch('https://...', { cache: 'force-cache' }) // Cached
 
  // ...
}

若要選擇將版面配置或頁面中的所有 fetch 請求加入快取,你可以使用 export const fetchCache = 'default-cache' 區段設定選項。如果個別的 fetch 請求指定了 cache 選項,則將改為使用該選項。

app/layout.js
// Since this is the root layout, all fetch requests in the app
// that don't set their own cache option will be cached.
export const fetchCache = 'default-cache'
 
export default async function RootLayout() {
  const a = await fetch('https://...') // Cached
  const b = await fetch('https://...', { cache: 'no-store' }) // Not cached
 
  // ...
}

路由處理器

路由處理器中的 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/fontcodemod 可用來安全且自動地重新命名你的匯入。

app/layout.js
// Before
import { Inter } from '@next/font/google'
 
// After
import { Inter } from 'next/font/google'

bundlePagesRouterDependencies

experimental.bundlePagesExternals 現在已穩定,並重新命名為 bundlePagesRouterDependencies

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Before
  experimental: {
    bundlePagesExternals: true,
  },
 
  // After
  bundlePagesRouterDependencies: true,
}
 
module.exports = nextConfig

serverExternalPackages

experimental.serverComponentsExternalPackages 現在已穩定,並重新命名為 serverExternalPackages

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Before
  experimental: {
    serverComponentsExternalPackages: ['package-name'],
  },
 
  // After
  serverExternalPackages: ['package-name'],
}
 
module.exports = nextConfig

Speed Insights

Next.js 15 中移除了 Speed Insights 的自動檢測功能。

若要繼續使用 Speed Insights,請依照 Vercel Speed Insights 快速入門 指南操作。

NextRequest 地理位置

NextRequest 上的 geoip 屬性已被移除,因為這些值是由你的託管供應商提供的。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)
 
  // ...
}