Как обновиться до версии 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, зависящие от информации во время выполнения, теперь стали асинхронными:
cookies
headers
draftMode
params
вlayout.js
,page.js
,route.js
,default.js
,opengraph-image
,twitter-image
,icon
, иapple-icon
.searchParams
вpage.js
Для упрощения миграции доступен кодмод, который автоматизирует процесс, и 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
}
Обработчики маршрутов
// До
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
}
// До
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'
.
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
, она будет использоваться вместо глобальной.
// Поскольку это корневой макет, все запросы 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'
, в файле обработчика маршрута.
export const dynamic = 'force-static'
export async function GET() {}
Клиентский кэш маршрутизатора
При переходе между страницами через <Link>
или useRouter
сегменты страниц больше не повторно используются из клиентского кэша маршрутизатора. Однако они по-прежнему повторно используются при навигации назад и вперёд в браузере, а также для общих макетов.
Чтобы включить кэширование сегментов страниц, вы можете использовать опцию конфигурации staleTimes
:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
staleTimes: {
dynamic: 30,
static: 180,
},
},
}
module.exports = nextConfig
Макеты и состояния загрузки по-прежнему кэшируются и повторно используются при навигации.
next/font
Пакет @next/font
был удалён в пользу встроенного next/font
. Доступен кодмод для безопасного и автоматического переименования ваших импортов.
// До
import { Inter } from '@next/font/google'
// После
import { Inter } from 'next/font/google'
bundlePagesRouterDependencies
experimental.bundlePagesExternals
теперь стал стабильным и переименован в bundlePagesRouterDependencies
.
/** @type {import('next').NextConfig} */
const nextConfig = {
// До
experimental: {
bundlePagesExternals: true,
},
// После
bundlePagesRouterDependencies: true,
}
module.exports = nextConfig
serverExternalPackages
experimental.serverComponentsExternalPackages
теперь стал стабильным и переименован в serverExternalPackages
.
/** @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
:
import { geolocation } from '@vercel/functions'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const { city } = geolocation(request)
// ...
}
import { ipAddress } from '@vercel/functions'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const ip = ipAddress(request)
// ...
}