How to upgrade to version 15
Source URL: https://nextjs.org/docs/app/guides/upgrading/version-15
How to upgrade to version 15
Section titled “How to upgrade to version 15”Upgrading from 14 to 15
Section titled “Upgrading from 14 to 15”To update to Next.js version 15, you can use the upgrade codemod:
pnpm dlx @next/codemod@canary upgrade latestnpx @next/codemod@canary upgrade latestyarn dlx @next/codemod@canary upgrade latestbunx @next/codemod@canary upgrade latestIf 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@latestnpm install next@latest react@latest react-dom@latest eslint-config-next@latestyarn add next@latest react@latest react-dom@latest eslint-config-next@latestbun add next@latest react@latest react-dom@latest eslint-config-next@latestGood 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
Section titled “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)
Section titled “Async Request APIs (Breaking change)”Previously synchronous Dynamic APIs that rely on runtime 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
Section titled “cookies”Recommended Async Usage
Section titled “Recommended Async Usage”import { cookies } from 'next/headers'
// Beforeconst cookieStore = cookies()const token = cookieStore.get('token')
// Afterconst cookieStore = await cookies()const token = cookieStore.get('token')Temporary Synchronous Usage
Section titled “Temporary Synchronous Usage”import { cookies, type UnsafeUnwrappedCookies } from 'next/headers'
// Beforeconst cookieStore = cookies()const token = cookieStore.get('token')
// Afterconst cookieStore = cookies() as unknown as UnsafeUnwrappedCookies// will log a warning in devconst token = cookieStore.get('token')import { cookies } from 'next/headers'
// Beforeconst cookieStore = cookies()const token = cookieStore.get('token')
// Afterconst cookieStore = cookies()// will log a warning in devconst token = cookieStore.get('token')headers
Section titled “headers”Recommended Async Usage
Section titled “Recommended Async Usage”import { headers } from 'next/headers'
// Beforeconst headersList = headers()const userAgent = headersList.get('user-agent')
// Afterconst headersList = await headers()const userAgent = headersList.get('user-agent')Temporary Synchronous Usage
Section titled “Temporary Synchronous Usage”import { headers, type UnsafeUnwrappedHeaders } from 'next/headers'
// Beforeconst headersList = headers()const userAgent = headersList.get('user-agent')
// Afterconst headersList = headers() as unknown as UnsafeUnwrappedHeaders// will log a warning in devconst userAgent = headersList.get('user-agent')import { headers } from 'next/headers'
// Beforeconst headersList = headers()const userAgent = headersList.get('user-agent')
// Afterconst headersList = headers()// will log a warning in devconst userAgent = headersList.get('user-agent')draftMode
Section titled “draftMode”Recommended Async Usage
Section titled “Recommended Async Usage”import { draftMode } from 'next/headers'
// Beforeconst { isEnabled } = draftMode()
// Afterconst { isEnabled } = await draftMode()Temporary Synchronous Usage
Section titled “Temporary Synchronous Usage”import { draftMode, type UnsafeUnwrappedDraftMode } from 'next/headers'
// Beforeconst { isEnabled } = draftMode()
// After// will log a warning in devconst { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftModeimport { draftMode } from 'next/headers'
// Beforeconst { isEnabled } = draftMode()
// After// will log a warning in devconst { isEnabled } = draftMode()params & searchParams
Section titled “params & searchParams”Asynchronous Layout
Section titled “Asynchronous Layout”// Beforetype 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}
// Aftertype 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}// Beforeexport function generateMetadata({ params }) { const { slug } = params}
export default async function Layout({ children, params }) { const { slug } = params}
// Afterexport async function generateMetadata({ params }) { const { slug } = await params}
export default async function Layout({ children, params }) { const { slug } = await params}Synchronous Layout
Section titled “Synchronous Layout”// Beforetype Params = { slug: string }
export default function Layout({ children, params,}: { children: React.ReactNode params: Params}) { const { slug } = params}
// Afterimport { 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}// Beforeexport default function Layout({ children, params }) { const { slug } = params}
// Afterimport { use } from 'react'export default async function Layout(props) { const params = use(props.params) const slug = params.slug}Asynchronous Page
Section titled “Asynchronous Page”// Beforetype 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}
// Aftertype 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}// Beforeexport function generateMetadata({ params, searchParams }) { const { slug } = params const { query } = searchParams}
export default function Page({ params, searchParams }) { const { slug } = params const { query } = searchParams}
// Afterexport 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}Synchronous Page
Section titled “Synchronous Page”'use client'
// Beforetype 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}
// Afterimport { 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}// Beforeexport default function Page({ params, searchParams }) { const { slug } = params const { query } = searchParams}
// Afterimport { 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
Section titled “Route Handlers”// Beforetype Params = { slug: string }
export async function GET(request: Request, segmentData: { params: Params }) { const params = segmentData.params const slug = params.slug}
// Aftertype Params = Promise<{ slug: string }>
export async function GET(request: Request, segmentData: { params: Params }) { const params = await segmentData.params const slug = params.slug}// Beforeexport async function GET(request, segmentData) { const params = segmentData.params const slug = params.slug}
// Afterexport async function GET(request, segmentData) { const params = await segmentData.params const slug = params.slug}runtime configuration (Breaking change)
Section titled “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
Section titled “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
Section titled “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-side Router Cache
Section titled “Client-side Router Cache”When navigating between pages via <Link> or useRouter, page segments are no longer reused from the client-side router 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 = nextConfigLayouts and loading states are still cached and reused on navigation.
next/font
Section titled “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.
// Beforeimport { Inter } from '@next/font/google'
// Afterimport { Inter } from 'next/font/google'bundlePagesRouterDependencies
Section titled “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 = nextConfigserverExternalPackages
Section titled “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 = nextConfigSpeed Insights
Section titled “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
Section titled “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)
// ...}