Как обновиться до версии 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

Полезно знать:

  • Если вы видите предупреждение о зависимостях, возможно, вам потребуется обновить react и react-dom до рекомендуемых версий, либо использовать флаги --force или --legacy-peer-deps, чтобы проигнорировать предупреждение. Это не понадобится, когда Next.js 15 и React 19 станут стабильными.

React 19

  • Минимальные версии react и react-dom теперь 19.
  • useFormState был заменён на useActionState. Хук useFormState всё ещё доступен в React 19, но считается устаревшим и будет удалён в будущих версиях. Рекомендуется использовать useActionState, который включает дополнительные свойства, такие как прямое чтение состояния pending. Подробнее.
  • useFormStatus теперь включает дополнительные ключи, такие как data, method и action. Если вы не используете React 19, доступен только ключ pending. Подробнее.
  • Читайте больше в руководстве по обновлению до React 19.

Полезно знать: Если вы используете TypeScript, убедитесь, что также обновили @types/react и @types/react-dom до их последних версий.

Асинхронные API запросов (Критическое изменение)

Ранее синхронные динамические 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')
import { cookies } from 'next/headers'

// До
const cookieStore = cookies()
const token = cookieStore.get('token')

// После
const cookieStore = cookies()
// будет выводить предупреждение в режиме разработки
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')
import { headers } from 'next/headers'

// До
const headersList = headers()
const userAgent = headersList.get('user-agent')

// После
const headersList = headers()
// будет выводить предупреждение в режиме разработки
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
import { draftMode } from 'next/headers'

// До
const { isEnabled } = draftMode()

// После
// будет выводить предупреждение в режиме разработки
const { isEnabled } = draftMode()

params и searchParams

Асинхронный Layout

// До
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
}
// До
export function generateMetadata({ params }) {
  const { slug } = params
}

export default async function Layout({ children, params }) {
  const { slug } = params
}

// После
export async function generateMetadata({ params }) {
  const { slug } = await params
}

export default async function Layout({ children, params }) {
  const { slug } = await params
}

Синхронный Layout

// До
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
}
// До
export default function Layout({ children, params }) {
  const { slug } = params
}

// После
import { use } from 'react'
export default async function Layout(props) {
  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
}
// До
export function generateMetadata({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}

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

// После
export async function generateMetadata(props) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}

export async function Page(props) {
  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 сегмента маршрута ранее поддерживала значение experimental-edge в дополнение к 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 в приложении,
// которые не устанавливают свою собственную опцию кэширования, будут кэшироваться.
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

Автоматическая инструментация для Speed Insights была удалена в Next.js 15.

Чтобы продолжить использовать Speed Insights, следуйте краткому руководству по Vercel Speed Insights.

Геолокация в NextRequest

Свойства geo и ip в NextRequest были удалены, так как эти значения теперь предоставляются вашим хостинг-провайдером. Для автоматизации миграции доступен кодмод.

Если вы используете Vercel, вы можете вместо этого использовать функции geolocation и ipAddress из @vercel/functions:

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)

  // ...
}