From 03d694b7adf00957cde779f3d23b8b71ccd117f2 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:40:15 -0700 Subject: [PATCH 1/4] Update RC migration guide Co-authored-by: Delba de Oliveira <32464864+delbaoliveira@users.noreply.github.com> --- .../11-upgrading/02-version-15.mdx | 482 +++++++++++++++++- 1 file changed, 477 insertions(+), 5 deletions(-) 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..f50b0fae4d4f8 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 @@ -30,16 +30,488 @@ 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 `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), [`default.js`](/docs/app/api-reference/file-conventions/default), [`opengraph-image`](/docs/app/api-reference/file-conventions/metadata/opengraph-image), [`twitter-image`](/docs/app/api-reference/file-conventions/metadata/opengraph-image), [`icon`](/docs/app/api-reference/file-conventions/metadata/app-icons), and [`apple-icon`](/docs/app/api-reference/file-conventions/metadata/app-icons). +- `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` + +```tsx +import { cookies } from 'next/headers' + +// Before +const cookieStore = cookies() +const token = cookieStore.get('token') + +// After (Preferred usage) +const cookieStore = await cookies() +const token = cookieStore.get('token') + +// After (Temporary supported API) +import { type DangerouslyUnwrapCookies } from 'next/headers' +const cookieStore = cookies() as unknown as DangerouslyUnwrapCookies +const token = cookieStore.get('token') +``` + +```jsx +// Before +const cookieStore = cookies() +const token = cookieStore.get('token') + +// After +const cookieStore = await cookies() +const token = cookieStore.get('token') +``` + +### `headers` + +```tsx +import { headers } from 'next/headers' + +// Before +const headersList = headers() +const userAgent = headersList.get('user-agent') + +// After (Preferred usage) +const headersList = await headers() +const userAgent = headersList.get('user-agent') + +// After (Temporary supported API) +import { type DangerouslyUnwrapHeaders } from 'next/headers' +const headersList = headers() as unknown as DangerouslyUnwrapHeaders +const userAgent = headersList.get('user-agent') +``` + +```jsx +// Before +const headersList = headers() +const userAgent = headersList.get('user-agent') + +// After +const headersList = await headers() +const userAgent = headersList.get('user-agent') +``` + +### `draftMode` + +```tsx +import { draftMode } from 'next/headers' + +// Before +const { isEnabled } = draftMode() + +// After (Preferred usage) +const { isEnabled } = await draftMode() + +// After (Temporary supported API) +import { type DangerouslyUnwrapDraftMode } from 'next/headers' +const { isEnabled } = draftMode() as unknown as DangerouslyUnwrapDraftMode +``` + +```jsx +// Before +const { isEnabled } = draftMode() + +// After +const { isEnabled } = await draftMode() +``` + +### `params` + +`layout` + +```tsx +// Before +type Params = { slug: string } +export function generateMetadata({ params }: { params: Params }) { + const slug = params.slug +} +export default function Layout({ + children, + params, +}: { + children: React.ReactNode + params: Params +}) { + const slug = params.slug +} + +// After (Preferred usage) +type Params = Promise<{ slug: string }> +export async function generateMetadata({ params }: { params: Params }) { + const slug = (await params).slug +} +export default async function Layout({ + children, + params, +}: { + children: React.ReactNode + params: Params +}) { + const slug = (await params).slug +} + +// After (Temporary supported API) +import { type DangerouslyUnwrapParams } from 'next/headers' +type Params = Promise<{ slug: string }> +export async function generateMetadata({ params }: { params: Params }) { + const syncParams = params as unknown as DangerouslyUnwrapParams + const slug = syncParams.slug +} +export default async function Layout({ + children, + params, +}: { + children: React.ReactNode + params: Params +}) { + const syncParams = params as unknown as DangerouslyUnwrapParams + const slug = syncParams.slug +} +``` + +```jsx +// Before +export function generateMetadata({ params }) { + const slug = params.slug +} +export default function Layout({ params }) { + const slug = params.slug +} + +// After +export async function generateMetadata({ params }) { + const slug = (await params).slug +} +export default async function Layout() { + const slug = (await params).slug +} + +``` + +`page` + +```tsx +// Before +type Params = { slug: string } +export function generateMetadata({ params }: { params: Params }) { + const slug = params.slug +} +export default function Page({ params }: { params: Params }) { + const slug = params.slug +} + +// After (Preferred usage) +type Params = Promise<{ slug: string }> +export async function generateMetadata({ params }: { params: Params }) { + const slug = (await params).slug +} +export default async function Page({ params }: { params: Params }) { + const slug = (await params).slug +} + +// After (Temporary supported API) +import { type DangerouslyUnwrapParams } from 'next/headers' +type Params = Promise<{ slug: string }> +export async function generateMetadata({ params }: { params: Params }) { + const syncParams = params as unknown as DangerouslyUnwrapParams + const slug = syncParams.slug +} +export default async function Page({ params }: { params: Params }) { + const syncParams = params as unknown as DangerouslyUnwrapParams + const slug = syncParams.slug +} +``` + +```jsx +// Before +export function generateMetadata({ params }) { + const slug = params.slug +} +export default function Page({ params }) { + const slug = params.slug +} + +// After +export async function generateMetadata({ params }) { + const slug = (await params).slug +} +export async function Page({ params }) { + const slug = (await params).slug +} +``` + +`layout` and `page` (Client Component) + +```tsx +'use client' + +// Before +type Params = { slug: string } +export default function Page({ params }: { params: Params }) { + const slug = params.slug +} + +// After +import { use } from 'react' +type Params = Promise<{ slug: string }> +export default function Page({ params }: { params: Params }) { + const slug = use(params).slug +} +``` + +```jsx +// Before +export default function Page({ params }) { +const slug = params.slug +} + +// After +import { use } from "react" +export default function Page({ params }) { +const slug = use(params).slug +} + +``` + +`default` + +```tsx +// Before +type Params = { artist: string } +export default function Default({ params }: { params: Params }) { + const artist = params.artist +} + +// After +type Params = Promise<{ artist: string }> +export default async function Default({ params }: { params: Params }) { + const artist = (await params).artist +} +``` + +```jsx +// Before +export default function Default({ params }) { + const artist = params.artist +} + +// After +export default async function Default({ params }) { + const artist = (await params).artist +} + +``` + +`route` + +``` +// Before +type Params = { slug: string } +export async function GET(request: Request, { params }: { params: Params }) { + const slug = params.slug +} + +// After (Preferred usage) +type Params = Promise<{ slug: string }> +export async function GET(request: Request, { params }: { params: Params }) { + const slug = (await params).slug +} + +// After (Temporary supported API) +import { type DangerouslyUnwrapParams } from "next/headers" +type Params = Promise<{ slug: string }> +export async function GET(request: Request, { params }: { params: Params }) { + const syncParams = params as unknown as DangerouslyUnwrapParams + const slug = syncParams.slug +} + +``` + +``` +// Before +export async function GET(request, { params }) { + const slug = params.slug +} + +// After +export async function GET(request, { params }) { + const slug = (await params).slug +} + +``` + +`opengraph-image`, `twitter-image`, `icon` and `apple-icon` + +```tsx +// Before +type Params = { slug: string } +export function generateImageMetadata({ params }: { params: Params }) { + const slug = params.slug +} +export function Image({ params }: { params: Params }) { + const slug = params.slug +} + +// After (Preferred usage) +type Params = Promise<{ slug: string }> +export async function generateImageMetadata({ params }: { params: Params }) { + const slug = (await params).slug +} +export async function Image({ params }: { params: Params }) { + const slug = (await params).slug +} + +// After (Temporary supported API) +import { type DangerouslyUnwrapParams } from 'next/headers' +type Params = Promise<{ slug: string }> +export async function generateImageMetadata({ params }: { params: Params }) { + const syncParams = params as unknown as DangerouslyUnwrapParams + const slug = syncParams.slug +} +export async function Image({ params }: { params: Params }) { + const syncParams = params as unknown as DangerouslyUnwrapParams + const slug = syncParams.slug +} +``` + +```jsx +// Before +export function generateImageMetadata({ params }) { + const slug = params.slug +} +export function Image({ params }) { + const slug = params.slug +} + +// After +export async function generateImageMetadata({ params }) { + const slug = (await params).slug +} +export async function Image({ params }) { + const slug = (await params).slug +} +``` + +### `searchParams` + +`page` + +```tsx +// Before +type SearchParams = { [key: string]: string | string[] | undefined } +export function generateMetadata({ + searchParams, +}: { + searchParams: SearchParams +}) { + const query = searchParams.query +} +export default function Page({ searchParams }: { searchParams: SearchParams }) { + const query = searchParams.query +} + +// After (Preferred usage) +type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> +export async function generateMetadata({ + searchParams, +}: { + searchParams: SearchParams +}) { + const query = (await searchParams).query +} +export async function Page({ searchParams }: { searchParams: SearchParams }) { + const query = (await searchParams).query +} + +// After (Temporary supported API) +import { type DangerouslyUnwrapSearchParams } from 'next/headers' +type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> +export async function generateMetadata({ + searchParams, +}: { + searchParams: SearchParams +}) { + const syncSearchParams = + searchParams as unknown as DangerouslyUnwrapSearchParams< + typeof searchParams + > + const query = syncSearchParams.query +} +export async function Page({ searchParams }: { searchParams: SearchParams }) { + const syncSearchParams = + searchParams as unknown as DangerouslyUnwrapSearchParams< + typeof searchParams + > + const query = syncSearchParams.query +} +``` + +```jsx +// Before +export function generateMetadata({ searchParams }) { + const query = searchParams.query +} +export default function Page({ searchParams }) { + const query = searchParams.query +} + +// After +export async function generateMetadata({ searchParams }) { + const query = (await searchParams).query +} +export async function Page({ searchParams }) { + const query = (await searchParams).query +} +``` + +`page` (Client Component) + +```tsx +'use client' + +// Before +type SearchParams = { [key: string]: string | string[] | undefined } +export default function Page({ searchParams }: { searchParams: SearchParams }) { + const query = searchParams.query +} + +// After +import { use } from 'react' +type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> +export default function Page({ searchParams }: { searchParams: SearchParams }) { + const query = use(searchParams).query +} +``` + +```jsx +// Before +export default function Page({ searchParams }) { + const query = searchParams.query +} + +// After +export default function Page({ searchParams }) { + const query = use(searchParams).query +} + +``` + ## `fetch` requests [`fetch` requests](/docs/app/api-reference/functions/fetch) are no longer cached by default. @@ -160,7 +632,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: From bcfc18586d397fc3b32555b57ee4c1c845801e39 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:59:26 -0700 Subject: [PATCH 2/4] update upgrade guide with latest information; condense sections --- .../11-upgrading/02-version-15.mdx | 467 ++++++++---------- 1 file changed, 197 insertions(+), 270 deletions(-) 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 f50b0fae4d4f8..b357fadcb4ec1 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 @@ -3,26 +3,18 @@ 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 `Content` 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, 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:** @@ -32,7 +24,7 @@ bun add next@rc react@rc react-dom@rc eslint-config-next@rc ## React 19 -- The minimum `react` and `react-dom` is now 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). @@ -44,13 +36,15 @@ Previously synchronous Dynamic APIs that rely on runtime information are now **a - [`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), [`default.js`](/docs/app/api-reference/file-conventions/default), [`opengraph-image`](/docs/app/api-reference/file-conventions/metadata/opengraph-image), [`twitter-image`](/docs/app/api-reference/file-conventions/metadata/opengraph-image), [`icon`](/docs/app/api-reference/file-conventions/metadata/app-icons), and [`apple-icon`](/docs/app/api-reference/file-conventions/metadata/app-icons). +- `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' @@ -58,28 +52,43 @@ import { cookies } from 'next/headers' const cookieStore = cookies() const token = cookieStore.get('token') -// After (Preferred usage) +// After const cookieStore = await cookies() const token = cookieStore.get('token') +``` + +#### Temporary Synchronous Usage -// After (Temporary supported API) -import { type DangerouslyUnwrapCookies } from 'next/headers' -const cookieStore = cookies() as unknown as DangerouslyUnwrapCookies +```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 +```jsx filename="app/page.js" switcher +import { cookies } from 'next/headers' + // Before const cookieStore = cookies() const token = cookieStore.get('token') // After -const cookieStore = await cookies() +const cookieStore = cookies() +// will log a warning in dev const token = cookieStore.get('token') ``` ### `headers` +#### Recommended Async Usage + ```tsx import { headers } from 'next/headers' @@ -87,61 +96,90 @@ import { headers } from 'next/headers' const headersList = headers() const userAgent = headersList.get('user-agent') -// After (Preferred usage) +// After const headersList = await headers() const userAgent = headersList.get('user-agent') +``` -// After (Temporary supported API) -import { type DangerouslyUnwrapHeaders } from 'next/headers' -const headersList = headers() as unknown as DangerouslyUnwrapHeaders +#### 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 +```jsx filename="app/page.js" switcher +import { headers } from 'next/headers' + // Before const headersList = headers() const userAgent = headersList.get('user-agent') // After -const headersList = await headers() +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 (Preferred usage) +// 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 (Temporary supported API) -import { type DangerouslyUnwrapDraftMode } from 'next/headers' -const { isEnabled } = draftMode() as unknown as DangerouslyUnwrapDraftMode +// After +// will log a warning in dev +const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode ``` -```jsx +```jsx filename="app/page.js" switcher +import { draftMode } from 'next/headers' + // Before const { isEnabled } = draftMode() // After -const { isEnabled } = await draftMode() +// will log a warning in dev +const { isEnabled } = draftMode() ``` -### `params` +### `params` & `searchParams` -`layout` +#### Asynchronous Layout -```tsx +```tsx filename="app/layout.tsx" switcher // Before type Params = { slug: string } + export function generateMetadata({ params }: { params: Params }) { const slug = params.slug } -export default function Layout({ + +export default async function Layout({ children, params, }: { @@ -151,28 +189,13 @@ export default function Layout({ const slug = params.slug } -// After (Preferred usage) +// After type Params = Promise<{ slug: string }> + export async function generateMetadata({ params }: { params: Params }) { const slug = (await params).slug } -export default async function Layout({ - children, - params, -}: { - children: React.ReactNode - params: Params -}) { - const slug = (await params).slug -} -// After (Temporary supported API) -import { type DangerouslyUnwrapParams } from 'next/headers' -type Params = Promise<{ slug: string }> -export async function generateMetadata({ params }: { params: Params }) { - const syncParams = params as unknown as DangerouslyUnwrapParams - const slug = syncParams.slug -} export default async function Layout({ children, params, @@ -180,17 +203,17 @@ export default async function Layout({ children: React.ReactNode params: Params }) { - const syncParams = params as unknown as DangerouslyUnwrapParams - const slug = syncParams.slug + const slug = (await params).slug } ``` -```jsx +```jsx filename="app/layout.js" switcher // Before export function generateMetadata({ params }) { const slug = params.slug } -export default function Layout({ params }) { + +export default async function Layout({ params }) { const slug = params.slug } @@ -198,318 +221,222 @@ export default function Layout({ params }) { export async function generateMetadata({ params }) { const slug = (await params).slug } -export default async function Layout() { + +export default async function Layout({ params }) { const slug = (await params).slug } ``` -`page` +#### Synchronous Layout -```tsx +```tsx filename="app/layout.tsx" switcher // Before type Params = { slug: string } -export function generateMetadata({ params }: { params: Params }) { - const slug = params.slug -} -export default function Page({ params }: { params: Params }) { + +export default function Layout({ + children, + params, +}: { + children: React.ReactNode + params: Params +}) { const slug = params.slug } -// After (Preferred usage) -type Params = Promise<{ slug: string }> -export async function generateMetadata({ params }: { params: Params }) { - const slug = (await params).slug -} -export default async function Page({ params }: { params: Params }) { - const slug = (await params).slug -} +// After +import { use } from 'react' -// After (Temporary supported API) -import { type DangerouslyUnwrapParams } from 'next/headers' type Params = Promise<{ slug: string }> -export async function generateMetadata({ params }: { params: Params }) { - const syncParams = params as unknown as DangerouslyUnwrapParams - const slug = syncParams.slug -} -export default async function Page({ params }: { params: Params }) { - const syncParams = params as unknown as DangerouslyUnwrapParams - const slug = syncParams.slug -} -``` - -```jsx -// Before -export function generateMetadata({ params }) { - const slug = params.slug -} -export default function Page({ params }) { - const slug = params.slug -} -// After -export async function generateMetadata({ params }) { - const slug = (await params).slug -} -export async function Page({ params }) { - const slug = (await params).slug +export default function Layout({ + children, + params, +}: { + children: React.ReactNode + params: Params +}) { + const slug = use(params).slug } ``` -`layout` and `page` (Client Component) - -```tsx -'use client' - +```jsx filename="app/layout.js" switcher // Before -type Params = { slug: string } -export default function Page({ params }: { params: Params }) { +export default function Layout({ params }) { const slug = params.slug } // After import { use } from 'react' -type Params = Promise<{ slug: string }> -export default function Page({ params }: { params: Params }) { +export default async function Layout({ params }) { const slug = use(params).slug } -``` - -```jsx -// Before -export default function Page({ params }) { -const slug = params.slug -} - -// After -import { use } from "react" -export default function Page({ params }) { -const slug = use(params).slug -} - -``` - -`default` - -```tsx -// Before -type Params = { artist: string } -export default function Default({ params }: { params: Params }) { - const artist = params.artist -} - -// After -type Params = Promise<{ artist: string }> -export default async function Default({ params }: { params: Params }) { - const artist = (await params).artist -} -``` - -```jsx -// Before -export default function Default({ params }) { - const artist = params.artist -} - -// After -export default async function Default({ params }) { - const artist = (await params).artist -} ``` -`route` +#### Asynchronous Page -``` +```tsx filename="app/page.tsx" switcher // Before type Params = { slug: string } -export async function GET(request: Request, { params }: { params: Params }) { - const slug = params.slug -} - -// After (Preferred usage) -type Params = Promise<{ slug: string }> -export async function GET(request: Request, { params }: { params: Params }) { - const slug = (await params).slug -} - -// After (Temporary supported API) -import { type DangerouslyUnwrapParams } from "next/headers" -type Params = Promise<{ slug: string }> -export async function GET(request: Request, { params }: { params: Params }) { - const syncParams = params as unknown as DangerouslyUnwrapParams - const slug = syncParams.slug -} - -``` +type SearchParams = { [key: string]: string | string[] | undefined } -``` -// Before -export async function GET(request, { params }) { +export function generateMetadata({ + params, + searchParams, +}: { + params: Params + searchParams: SearchParams +}) { const slug = params.slug + const query = searchParams.query } -// After -export async function GET(request, { params }) { - const slug = (await params).slug -} - -``` - -`opengraph-image`, `twitter-image`, `icon` and `apple-icon` - -```tsx -// Before -type Params = { slug: string } -export function generateImageMetadata({ params }: { params: Params }) { - const slug = params.slug -} -export function Image({ params }: { params: Params }) { +export default async function Page({ + params, + searchParams, +}: { + params: Params + searchParams: SearchParams +}) { const slug = params.slug + const query = searchParams.query } -// After (Preferred usage) +// After type Params = Promise<{ slug: string }> -export async function generateImageMetadata({ params }: { params: Params }) { - const slug = (await params).slug -} -export async function Image({ params }: { params: Params }) { +type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> + +export async function generateMetadata({ + params, + searchParams, +}: { + params: Params + searchParams: SearchParams +}) { const slug = (await params).slug + const query = (await searchParams).query } -// After (Temporary supported API) -import { type DangerouslyUnwrapParams } from 'next/headers' -type Params = Promise<{ slug: string }> -export async function generateImageMetadata({ params }: { params: Params }) { - const syncParams = params as unknown as DangerouslyUnwrapParams - const slug = syncParams.slug -} -export async function Image({ params }: { params: Params }) { - const syncParams = params as unknown as DangerouslyUnwrapParams - const slug = syncParams.slug +export default async function Page({ + params, + searchParams, +}: { + params: Params + searchParams: SearchParams +}) { + const slug = (await params).slug + const query = (await searchParams).query } ``` -```jsx +```jsx filename="app/page.js" switcher // Before -export function generateImageMetadata({ params }) { +export function generateMetadata({ params, searchParams }) { const slug = params.slug + const query = searchParams.query } -export function Image({ params }) { + +export default function Page({ params, searchParams }) { const slug = params.slug + const query = searchParams.query } // After -export async function generateImageMetadata({ params }) { +export async function generateMetadata({ params, searchParams }) { const slug = (await params).slug + const query = (await searchParams).query } -export async function Image({ params }) { + +export async function Page({ params, searchParams }) { const slug = (await params).slug + const query = (await searchParams).query } ``` -### `searchParams` - -`page` +#### Synchronous Page ```tsx +'use client' + // Before +type Params = { slug: string } type SearchParams = { [key: string]: string | string[] | undefined } -export function generateMetadata({ + +export default function Page({ + params, searchParams, }: { + params: Params searchParams: SearchParams }) { - const query = searchParams.query -} -export default function Page({ searchParams }: { searchParams: SearchParams }) { + const slug = params.slug const query = searchParams.query } -// After (Preferred usage) -type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> -export async function generateMetadata({ - searchParams, -}: { - searchParams: SearchParams -}) { - const query = (await searchParams).query -} -export async function Page({ searchParams }: { searchParams: SearchParams }) { - const query = (await searchParams).query -} +// After +import { use } from 'react' -// After (Temporary supported API) -import { type DangerouslyUnwrapSearchParams } from 'next/headers' -type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> -export async function generateMetadata({ +type Params = Promise<{ slug: string }> +type SearchParams = { [key: string]: string | string[] | undefined } + +export default function Page({ + params, searchParams, }: { + params: Params searchParams: SearchParams }) { - const syncSearchParams = - searchParams as unknown as DangerouslyUnwrapSearchParams< - typeof searchParams - > - const query = syncSearchParams.query -} -export async function Page({ searchParams }: { searchParams: SearchParams }) { - const syncSearchParams = - searchParams as unknown as DangerouslyUnwrapSearchParams< - typeof searchParams - > - const query = syncSearchParams.query + const slug = use(params).slug + const query = use(searchParams).query } ``` ```jsx // Before -export function generateMetadata({ searchParams }) { - const query = searchParams.query -} -export default function Page({ searchParams }) { +export default function Page({ params, searchParams }) { + const slug = params.slug const query = searchParams.query } // After -export async function generateMetadata({ searchParams }) { - const query = (await searchParams).query -} -export async function Page({ searchParams }) { - const query = (await searchParams).query +import { use } from "react" + +export default function Page({ params, searchParams }) { + const slug = use(params).slug + const query = use(searchParams).query } -``` -`page` (Client Component) +``` -```tsx -'use client' +#### Route Handlers +```tsx filename="app/api/route.ts" // Before -type SearchParams = { [key: string]: string | string[] | undefined } -export default function Page({ searchParams }: { searchParams: SearchParams }) { - const query = searchParams.query +type Params = { slug: string } + +export async function GET(request: Request, { params }: { params: Params }) { + const slug = params.slug } // After -import { use } from 'react' -type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> -export default function Page({ searchParams }: { searchParams: SearchParams }) { - const query = use(searchParams).query +type Params = Promise<{ slug: string }> + +export async function GET(request: Request, { params }: { params: Params }) { + const slug = (await params).slug } ``` -```jsx +```js filename="app/api/route.js" // Before -export default function Page({ searchParams }) { - const query = searchParams.query +export async function GET(request, { params }) { + const slug = params.slug } // After -export default function Page({ searchParams }) { - const query = use(searchParams).query +export async function GET(request, { params }) { + const slug = (await params).slug } - ``` ## `fetch` requests From 7ef955e44dfb93bb31f0a8bca39caa8d893f4b1f Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:42:37 -0700 Subject: [PATCH 3/4] add breaking experimental-edge change --- .../11-upgrading/02-version-15.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 b357fadcb4ec1..a7835d7200fcd 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 @@ -3,6 +3,8 @@ 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 `Content` 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: @@ -439,6 +441,14 @@ export async function GET(request, { params }) { } ``` + + +## `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. From 333136f5c053b25c7766085d049f38caa4fef2de Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:45:06 -0700 Subject: [PATCH 4/4] refactor destructuring --- .../11-upgrading/02-version-15.mdx | 137 +++++++++--------- 1 file changed, 71 insertions(+), 66 deletions(-) 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 a7835d7200fcd..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 @@ -178,7 +178,7 @@ const { isEnabled } = draftMode() type Params = { slug: string } export function generateMetadata({ params }: { params: Params }) { - const slug = params.slug + const { slug } = params } export default async function Layout({ @@ -188,14 +188,14 @@ export default async function Layout({ children: React.ReactNode params: Params }) { - const slug = params.slug + const { slug } = params } // After type Params = Promise<{ slug: string }> export async function generateMetadata({ params }: { params: Params }) { - const slug = (await params).slug + const { slug } = await params } export default async function Layout({ @@ -205,29 +205,28 @@ export default async function Layout({ children: React.ReactNode params: Params }) { - const slug = (await params).slug + const { slug } = await props } ``` ```jsx filename="app/layout.js" switcher // Before export function generateMetadata({ params }) { - const slug = params.slug + const { slug } = params } -export default async function Layout({ params }) { - const slug = params.slug +export default async function Layout({ children, params }) { + const { slug } = params } // After export async function generateMetadata({ params }) { - const slug = (await params).slug + const { slug } = await params } -export default async function Layout({ params }) { - const slug = (await params).slug +export default async function Layout({ children, params }) { + const { slug } = await params } - ``` #### Synchronous Layout @@ -243,7 +242,7 @@ export default function Layout({ children: React.ReactNode params: Params }) { - const slug = params.slug + const { slug } = params } // After @@ -251,27 +250,26 @@ import { use } from 'react' type Params = Promise<{ slug: string }> -export default function Layout({ - children, - params, -}: { +export default function Layout(props: { children: React.ReactNode params: Params }) { - const slug = use(params).slug + const params = use(props.params) + const slug = params.slug } ``` ```jsx filename="app/layout.js" switcher // Before -export default function Layout({ params }) { - const slug = params.slug +export default function Layout({ children, params }) { + const { slug } = params } // After import { use } from 'react' -export default async function Layout({ params }) { - const slug = use(params).slug +export default async function Layout(props) { + const params = use(props.params) + const slug = params.slug } ``` @@ -290,8 +288,8 @@ export function generateMetadata({ params: Params searchParams: SearchParams }) { - const slug = params.slug - const query = searchParams.query + const { slug } = params + const { query } = searchParams } export default async function Page({ @@ -301,58 +299,60 @@ export default async function Page({ params: Params searchParams: SearchParams }) { - const slug = params.slug - const query = searchParams.query + const { slug } = params + const { query } = searchParams } // After type Params = Promise<{ slug: string }> type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> -export async function generateMetadata({ - params, - searchParams, -}: { +export async function generateMetadata(props: { params: Params searchParams: SearchParams }) { - const slug = (await params).slug - const query = (await searchParams).query + const params = await props.params + const searchParams = await props.searchParams + const slug = params.slug + const query = searchParams.query } -export default async function Page({ - params, - searchParams, -}: { +export default async function Page(props: { params: Params searchParams: SearchParams }) { - const slug = (await params).slug - const query = (await searchParams).query + 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.slug - const query = searchParams.query + const { slug } = params + const { query } = searchParams } export default function Page({ params, searchParams }) { - const slug = params.slug - const query = searchParams.query + const { slug } = params + const { query } = searchParams } // After -export async function generateMetadata({ params, searchParams }) { - const slug = (await params).slug - const query = (await searchParams).query +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({ params, searchParams }) { - const slug = (await params).slug - const query = (await 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 } ``` @@ -372,8 +372,8 @@ export default function Page({ params: Params searchParams: SearchParams }) { - const slug = params.slug - const query = searchParams.query + const { slug } = params + const { query } = searchParams } // After @@ -382,31 +382,32 @@ import { use } from 'react' type Params = Promise<{ slug: string }> type SearchParams = { [key: string]: string | string[] | undefined } -export default function Page({ - params, - searchParams, -}: { +export default function Page(props: { params: Params searchParams: SearchParams }) { - const slug = use(params).slug - const query = use(searchParams).query + 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.slug - const query = searchParams.query + const { slug } = params + const { query } = searchParams } // After import { use } from "react" -export default function Page({ params, searchParams }) { - const slug = use(params).slug - const query = use(searchParams).query +export default function Page(props) { + const params = use(props.params) + const searchParams = use(props.searchParams) + const slug = params.slug + const query = searchParams.query } ``` @@ -417,27 +418,31 @@ export default function Page({ params, searchParams }) { // Before type Params = { slug: string } -export async function GET(request: Request, { params }: { params: Params }) { +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, { params }: { params: Params }) { - const slug = (await params).slug +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, { params }) { +export async function GET(request, segmentData) { + const params = segmentData.params const slug = params.slug } // After -export async function GET(request, { params }) { - const slug = (await params).slug +export async function GET(request, segmentData) { + const params = await segmentData.params + const slug = params.slug } ```