vercel/next.js / docs / 01-app / 02-guides / upgrading / version-15.mdx
The React Framework
version-15.mdx
title: How to upgrade to version 15 nav_title: Version 15 description: Upgrade your Next.js Application from Version 14 to 15.
{/* The content of this doc is shared between the app and pages router. You can use the <PagesOnly>Content</PagesOnly> component to add content that is specific to the Pages Router. Any shared content should not be wrapped in a component. */}
Upgrading from 14 to 15
To update to Next.js version 15, you can use the upgrade codemod:
pnpm dlx @next/codemod@canary upgrade latest
npx @next/codemod@canary upgrade latest
yarn dlx @next/codemod@canary upgrade latest
bunx @next/codemod@canary upgrade latest
If you prefer to do it manually, ensure that you're installing the latest Next & React versions:
pnpm add next@latest react@latest react-dom@latest eslint-config-next@latest
npm install next@latest react@latest react-dom@latest eslint-config-next@latest
yarn add next@latest react@latest react-dom@latest eslint-config-next@latest
bun add next@latest react@latest react-dom@latest eslint-config-next@latest
Good to know:
- If you see a peer dependencies warning, you may need to update
reactandreact-domto the suggested versions, or use the--forceor--legacy-peer-depsflag to ignore the warning. This won't be necessary once both Next.js 15 and React 19 are stable.
React 19
- The minimum versions of
reactandreact-domis now 19. useFormStatehas been replaced byuseActionState. TheuseFormStatehook is still available in React 19, but it is deprecated and will be removed in a future release.useActionStateis recommended and includes additional properties like reading thependingstate directly. Learn more.useFormStatusnow includes additional keys likedata,method, andaction. If you are not using React 19, only thependingkey is available. Learn more.- Read more in the React 19 upgrade guide.
Good to know: If you are using TypeScript, ensure you also upgrade
@types/reactand@types/react-domto their latest versions.
Async Request APIs (Breaking change)
Previously synchronous Request-time APIs that rely on request information are now asynchronous:
cookiesheadersdraftModeparamsinlayout.js,page.js,route.js,default.js,opengraph-image,twitter-image,icon, andapple-icon.searchParamsinpage.js
To ease the burden of migration, a codemod is available to automate the process and the APIs can temporarily be accessed synchronously.
cookies
Recommended Async Usage
import { cookies } from 'next/headers'
// Before
const cookieStore = cookies()
const token = cookieStore.get('token')
// After
const cookieStore = await cookies()
const token = cookieStore.get('token')
Temporary Synchronous Usage
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')
import { cookies } from 'next/headers'
// Before
const cookieStore = cookies()
const token = cookieStore.get('token')
// After
const cookieStore = cookies()
// will log a warning in dev
const token = cookieStore.get('token')
headers
Recommended Async Usage
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')
Temporary Synchronous Usage
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')
import { headers } from 'next/headers'
// Before
const headersList = headers()
const userAgent = headersList.get('user-agent')
// After
const headersList = headers()
// will log a warning in dev
const userAgent = headersList.get('user-agent')
draftMode
Recommended Async Usage
import { draftMode } from 'next/headers'
// Before
const { isEnabled } = draftMode()
// After
const { isEnabled } = await draftMode()
Temporary Synchronous Usage
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
import { draftMode } from 'next/headers'
// Before
const { isEnabled } = draftMode()
// After
// will log a warning in dev
const { isEnabled } = draftMode()
params & searchParams
Asynchronous Layout
// 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
}
// Before
export function generateMetadata({ params }) {
const { slug } = params
}
export default async function Layout({ children, params }) {
const { slug } = params
}
// After
export async function generateMetadata({ params }) {
const { slug } = await params
}
export default async function Layout({ children, params }) {
const { slug } = await params
}
Synchronous Layout
// 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
}
// Before
export default function Layout({ children, params }) {
const { slug } = params
}
// After
import { use } from 'react'
export default async function Layout(props) {
const params = use(props.params)
const slug = params.slug
}
Asynchronous Page
// 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
}
// Before
export function generateMetadata({ params, searchParams }) {
const { slug } = params
const { query } = searchParams
}
export default function Page({ params, searchParams }) {
const { slug } = params
const { query } = searchParams
}
// After
export async function generateMetadata(props) {
const params = await props.params
const searchParams = await props.searchParams
const slug = params.slug
const query = searchParams.query
}
export default async function Page(props) {
const params = await props.params
const searchParams = await props.searchParams
const slug = params.slug
const query = searchParams.query
}
Synchronous Page
'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
}
Route Handlers
// 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
}
// 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
}
<AppOnly>
runtime configuration (Breaking change)
The runtime segment configuration previously supported a value of experimental-edge in addition to edge. Both configurations refer to the same thing, and to simplify the options, we will now error if experimental-edge is used. To fix this, update your runtime configuration to edge. A codemod is available to automatically do this.
fetch requests
fetch requests are no longer cached by default.
To opt specific fetch requests into caching, you can pass the cache: 'force-cache' option.
export default async function RootLayout() {
const a = await fetch('https://...') // Not Cached
const b = await fetch('https://...', { cache: 'force-cache' }) // Cached
// ...
}
To opt all fetch requests in a layout or page into caching, you can use the export const fetchCache = 'default-cache' segment config option. If individual fetch requests specify a cache option, that will be used instead.
// 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
// ...
}
Route Handlers
GET functions in Route Handlers are no longer cached by default. To opt GET methods into caching, you can use a route config option such as export const dynamic = 'force-static' in your Route Handler file.
export const dynamic = 'force-static'
export async function GET() {}
Client Cache
When navigating between pages via <Link> or useRouter, page segments are no longer reused from the Client Cache. However, they are still reused during browser backward and forward navigation and for shared layouts.
To opt page segments into caching, you can use the staleTimes config option:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
staleTimes: {
dynamic: 30,
static: 180,
},
},
}
module.exports = nextConfig
Layouts and loading states are still cached and reused on navigation.
next/font
The @next/font package has been removed in favor of the built-in next/font. A codemod is available to safely and automatically rename your imports.
// Before
import { Inter } from '@next/font/google'
// After
import { Inter } from 'next/font/google'
bundlePagesRouterDependencies
experimental.bundlePagesExternals is now stable and renamed to bundlePagesRouterDependencies.
/** @type {import('next').NextConfig} */
const nextConfig = {
// Before
experimental: {
bundlePagesExternals: true,
},
// After
bundlePagesRouterDependencies: true,
}
module.exports = nextConfig
serverExternalPackages
experimental.serverComponentsExternalPackages is now stable and renamed to serverExternalPackages.
/** @type {import('next').NextConfig} */
const nextConfig = {
// Before
experimental: {
serverComponentsExternalPackages: ['package-name'],
},
// After
serverExternalPackages: ['package-name'],
}
module.exports = nextConfig
Speed Insights
Auto instrumentation for Speed Insights was removed in Next.js 15.
To continue using Speed Insights, follow the Vercel Speed Insights Quickstart guide.
NextRequest Geolocation
The geo and ip properties on NextRequest have been removed as these values are provided by your hosting provider. A codemod is available to automate this migration.
If you are using Vercel, you can alternatively use the geolocation and ipAddress functions from @vercel/functions instead:
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)
// ...
}
Troubleshooting
Installation fails after upgrading to React 19
- If another package's peer dependencies reject React 19, revert
react,react-dom, their types, andreact-is(if present) to the previous versions, provided the target Next.js version and router support them. - Restore or remove React type overrides or resolutions added by the upgrade in
package.jsonorpnpm-workspace.yaml. - If installation interrupted the upgrade command, retry the same exact target with
--skip-react-upgradeto preserve the repaired dependencies. If Next.js already matches the target, install dependencies and run any skipped codemods individually. Run the app's checks.
Installation fails after upgrading to ESLint 9
- If another package's peer dependencies reject ESLint 9, restore the previous compatible versions of
eslint,eslint-config-next, and@next/eslint-plugin-next(if present), along with the previous lint configuration. - If installation interrupted the upgrade command, retry the same exact target with
--skip-eslint-upgradeto preserve the repaired dependency. If Next.js already matches the target, install dependencies and run any skipped codemods individually. Complete any required lint migration manually, then run the app's lint checks.
Review migration checklist
After applying the changes relevant to your app, review these action items before you consider the migration complete:
- React dependencies and types meet the React 19 requirements for the app's router.
- Async Request API access, including affected helpers and callers, is migrated. Resolve codemod error comments and
UnsafeUnwrappedcasts instead of treating them as completed repairs. - Route segment runtime settings no longer use
experimental-edge. - Font imports use
next/fontinstead of@next/font. - Existing
bundlePagesExternalsandserverComponentsExternalPackagessettings use their documented stable names while preserving package configuration. -
NextRequestgeoandipconsumers use an appropriate source for the actual hosting provider. - Apps that relied on automatic Speed Insights instrumentation have an explicit integration if they still need it.
- The app's supported checks and affected runtime behavior pass. Remaining required failures keep the migration incomplete.
Rendered from the public repository vercel/next.js on GitHub; content belongs to its authors. Cached for up to an hour.