diff --git a/docs/02-app/01-building-your-application/11-upgrading/02-version-15.mdx b/docs/02-app/01-building-your-application/11-upgrading/02-version-15.mdx index 337eff87f04b4..a48dd71084e15 100644 --- a/docs/02-app/01-building-your-application/11-upgrading/02-version-15.mdx +++ b/docs/02-app/01-building-your-application/11-upgrading/02-version-15.mdx @@ -7,22 +7,16 @@ description: Upgrade your Next.js Application from Version 14 to 15. ## Upgrading from 14 to 15 -To update to Next.js version 15, run the following command using your preferred package manager: +To update to Next.js version 15, you can use the `upgrade` codemod: ```bash filename="Terminal" -npm i next@rc react@rc react-dom@rc eslint-config-next@rc +npx @next/codemod@canary upgrade ``` -```bash filename="Terminal" -yarn add next@rc react@rc react-dom@rc eslint-config-next@rc -``` - -```bash filename="Terminal" -pnpm up next@rc react@rc react-dom@rc eslint-config-next@rc -``` +If you prefer to do it manually, ensure that you're installing the latest Next & React RC, e.g.: ```bash filename="Terminal" -bun add next@rc react@rc react-dom@rc eslint-config-next@rc +npm i next@canary react@rc react-dom@rc eslint-config-next@rc ``` > **Good to know:** @@ -30,16 +24,436 @@ bun add next@rc react@rc react-dom@rc eslint-config-next@rc > - If you see a peer dependencies warning, you may need to update `react` and `react-dom` to the suggested versions, or you use the `--force` or `--legacy-peer-deps` flag to ignore the warning. This won't be necessary once both Next.js 15 and React 19 are stable. > - If you are using TypeScript, you'll need to temporarily override the React types. See the [React 19 RC upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide#installing) for more information. -## Minimum React version - -- The minimum `react` and `react-dom` is now 19. - ## React 19 +- The minimum versions of `react` and `react-dom` is now 19. - `useFormState` has been replaced by `useActionState`. The `useFormState` hook is still available in React 19, but it is deprecated and will be removed in a future release. `useActionState` is recommended and includes additional properties like reading the `pending` state directly. [Learn more](https://react.dev/reference/react/useActionState). - `useFormStatus` now includes additional keys like `data`, `method`, and `action`. If you are not using React 19, only the `pending` key is available. [Learn more](https://react.dev/reference/react-dom/hooks/useFormStatus). - Read more in the [React 19 upgrade guide](https://react.dev/blog/2024/04/25/react-19-upgrade-guide). +## Async Request APIs (Breaking change) + +Previously synchronous Dynamic APIs that rely on runtime information are now **asynchronous**: + +- [`cookies`](/docs/app/api-reference/functions/cookies) +- [`headers`](/docs/app/api-reference/functions/headers) +- [`draftMode`](/docs/app/api-reference/functions/draft-mode) +- `params` in [`layout.js`](/docs/app/api-reference/file-conventions/layout), [`page.js`](/docs/app/api-reference/file-conventions/page), [`route.js`](/docs/app/api-reference/file-conventions/route), and [`default.js`](/docs/app/api-reference/file-conventions/default). +- `searchParams` in [`page.js`](/docs/app/api-reference/file-conventions/page) + +To ease the burden of migration, a [codemod is available](/docs/app/building-your-application/upgrading/codemods#150) to automate the process and the APIs can temporarily be accessed synchronously. + +### `cookies` + +#### Recommended Async Usage + +```tsx +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 + +```tsx filename="app/page.tsx" switcher +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') +``` + +```jsx filename="app/page.js" switcher +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 + +```tsx +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 + +```tsx filename="app/page.tsx" switcher +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') +``` + +```jsx filename="app/page.js" switcher +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 + +```tsx +import { draftMode } from 'next/headers' + +// Before +const { isEnabled } = draftMode() + +// After +const { isEnabled } = await draftMode() +``` + +#### Temporary Synchronous Usage + +```tsx filename="app/page.tsx" switcher +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 +``` + +```jsx filename="app/page.js" switcher +import { draftMode } from 'next/headers' + +// Before +const { isEnabled } = draftMode() + +// After +// will log a warning in dev +const { isEnabled } = draftMode() +``` + +### `params` & `searchParams` + +#### Asynchronous Layout + +```tsx filename="app/layout.tsx" switcher +// 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 props +} +``` + +```jsx filename="app/layout.js" switcher +// 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 + +```tsx filename="app/layout.tsx" switcher +// 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 +} +``` + +```jsx filename="app/layout.js" switcher +// 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 + +```tsx filename="app/page.tsx" switcher +// 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 +} +``` + +```jsx filename="app/page.js" switcher +// 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 async function Page(props) { + const params = await props.params + const searchParams = await props.searchParams + const slug = params.slug + const query = searchParams.query +} +``` + +#### Synchronous Page + +```tsx +'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 = { [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 +} +``` + +```jsx +// 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 + +```tsx filename="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 +} +``` + +```js filename="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` configuration (Breaking change) + +The `runtime` [segment configuration](/docs/app/api-reference/file-conventions/route-segment-config#runtime) 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](/docs/app/building-your-application/upgrading/codemods#app-dir-runtime-config-experimental-edge) is available to automatically do this. + + + ## `fetch` requests [`fetch` requests](/docs/app/api-reference/functions/fetch) are no longer cached by default. @@ -160,7 +574,7 @@ To continue using Speed Insights, follow the [Vercel Speed Insights Quickstart]( ## `NextRequest` Geolocation -The `geo` and `ip` properties on `NextRequest` have been removed. These values had to be provided by your hosting provider. +The `geo` and `ip` properties on `NextRequest` have been removed as these values are provided by your hosting provider. A [codemod](/docs/app/building-your-application/upgrading/codemods#150) is available to automate this migration. If you are using Vercel, you can alternatively use the `geolocation` and `ipAddress` functions from [`@vercel/functions](https://vercel.com/docs/functions/vercel-functions-package) instead: