Skip to content

Commit

Permalink
condense warnings for direct access of dynamic APIs (#70682)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon authored Oct 2, 2024
1 parent 349dd1f commit 772b0ea
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 87 deletions.
72 changes: 72 additions & 0 deletions errors/sync-dynamic-apis.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: dynamic APIs are async
description: This is a migration guide for dynamic APIs that have become async when access was previously possible directly.
---

## Why This Warning Occurred

Somewhere in your code you used a dynamic API and accessed one of its properties directly.
Dynamic APIs are:

- `params` and `searchParams`
- `cookies()`, `draftMode()`, and `headers()` from `next/headers`

For example:

```js filename="app/[id]/page.js"
function Page({ params }) {
// direct access of `params.id`.
return <p>ID: {params.id}</p>
}
```

This also includes enumerating (e.g. `{...params}`, or `Object.keys(params)`) or iterating over the return
value of these APIs (e.g. `[...headers()]` or `for (const cookie of cookies())`, or explicitly with `cookies()[Symbol.iterator]()`).

In the version of Next.js that issued this warning, access to these properties is still possible directly but will warn.
In future versions, these APIs will be async and direct access will not work as expected.

## Possible Ways to Fix It

The `next-async-request-api` codemod can fix many of these cases automatically:

```bash
$ npx @next/codemod@canary next-async-request-api .
```

The codemod cannot cover all cases, so you may need to manually adjust some code.

The dynamic APIs are now async and return a Promise.

If the warning occured on the Server (e.g. a route handler, or a Server Component),
you must `await` the dynamic API to access its properties:

```js filename="app/[id]/page.js"


function Page({ params }) {
// asynchronous access of `params.id`.
const { id } = await params
return <p>ID: {id}</p>
}
```

If the warning occured on the Client (e.g. a Client component),
you must use `React.use()` to unwrap the Promise first:

```js filename="app/[id]/page.js"
'use client'
import * as React from 'react'

function Page({ params }) {
// asynchronous access of `params.id`.
const { id } = React.use(params)
return <p>ID: {id}</p>
}
```

Keep in mind that you can delay the unwrapping (either via `await` or `React.use`)
until further down in your component tree when you actually need the value.
You don't have to unwrap the Promise immediately at the segment level (Page, Layout, etc).

Unwraping the Promise later will allow Next.js to statically render more of your page before the Page is actually requested.
8 changes: 6 additions & 2 deletions packages/next/src/server/request/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,14 +513,18 @@ function describeNameArg(arg: unknown) {
function warnForSyncIteration(route?: string) {
const prefix = route ? ` In route ${route} ` : ''
console.error(
`${prefix}cookies were iterated implicitly with something like \`for...of cookies())\` or \`[...cookies()]\`, or explicitly with \`cookies()[Symbol.iterator]()\`. \`cookies()\` now returns a Promise and the return value should be awaited before attempting to iterate over cookies. In this version of Next.js iterating cookies without awaiting first is still supported to facilitate migration but in a future version you will be required to await the result. If this \`cookies()\` use is inside an async function await the return value before accessing attempting iteration. If this use is inside a synchronous function then convert the function to async or await the call from outside this function and pass the result in.`
`${prefix}cookies were iterated over. ` +
`\`cookies()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}

function warnForSyncAccess(route: undefined | string, expression: string) {
const prefix = route ? ` In route ${route} a ` : 'A '
console.error(
`${prefix}cookie property was accessed directly with \`${expression}\`. \`cookies()\` now returns a Promise and the return value should be awaited before accessing properties of the underlying cookies instance. In this version of Next.js direct access to \`${expression}\` is still supported to facilitate migration but in a future version you will be required to await the result. If this \`cookies()\` use is inside an async function await the return value before accessing attempting iteration. If this use is inside a synchronous function then convert the function to async or await the call from outside this function and pass the result in.`
`${prefix}cookie property was accessed directly with \`${expression}\`. ` +
`\`cookies()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}

Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/server/request/draft-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ class DraftMode {
function warnForSyncAccess(route: undefined | string, expression: string) {
const prefix = route ? ` In route ${route} a ` : 'A '
console.error(
`${prefix}\`draftMode()\` property was accessed directly with \`${expression}\`. \`draftMode()\` now returns a Promise and the return value should be awaited before accessing properties of the underlying draftMode object. In this version of Next.js direct access to \`${expression}\` is still supported to facilitate migration but in a future version you will be required to await the result. If this \`draftMode()\` use is inside an async function await the return value before accessing attempting iteration. If this use is inside a synchronous function then convert the function to async or await the call from outside this function and pass the result in.`
`${prefix}\`draftMode()\` property was accessed directly with \`${expression}\`. ` +
`\`draftMode()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/draft-mode-sync-access`
)
}
8 changes: 6 additions & 2 deletions packages/next/src/server/request/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,14 +428,18 @@ function describeNameArg(arg: unknown) {
function warnForSyncIteration(route?: string) {
const prefix = route ? ` In route ${route} ` : ''
console.error(
`${prefix}headers were iterated implicitly with something like \`for...of headers())\` or \`[...headers()]\`, or explicitly with \`headers()[Symbol.iterator]()\`. \`headers()\` now returns a Promise and the return value should be awaited before attempting to iterate over headers. In this version of Next.js iterating headers without awaiting first is still supported to facilitate migration but in a future version you will be required to await the result. If this \`headers()\` use is inside an async function await the return value before accessing attempting iteration. If this use is inside a synchronous function then convert the function to async or await the call from outside this function and pass the result in.`
`${prefix}headers were iterated over. ` +
`\`headers()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}

function warnForSyncAccess(route: undefined | string, expression: string) {
const prefix = route ? ` In route ${route} a ` : 'A '
console.error(
`${prefix}header property was accessed directly with \`${expression}\`. \`headers()\` now returns a Promise and the return value should be awaited before accessing properties of the underlying headers instance. In this version of Next.js direct access to \`${expression}\` is still supported to facilitate migration but in a future version you will be required to await the result. If this \`headers()\` use is inside an async function await the return value before accessing attempting iteration. If this use is inside a synchronous function then convert the function to async or await the call from outside this function and pass the result in.`
`${prefix}header property was accessed directly with \`${expression}\`. ` +
`\`headers()\` should be awaited before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}

Expand Down
8 changes: 6 additions & 2 deletions packages/next/src/server/request/params.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,15 @@ function warnForEnumeration(missingProperties: Array<string>) {
const describedMissingProperties =
describeListOfPropertyNames(missingProperties)
console.error(
`params are being enumerated incompletely with \`{...params}\`, \`Object.keys(params)\`, or similar. The following properties were not copied: ${describedMissingProperties}. \`params\` is now a Promise, however in the current version of Next.js direct access to the underlying params object is still supported to facilitate migration to the new type. param names that conflict with Promise properties cannot be accessed directly and must be accessed by first unwrapping the \`params\` promise with \`React.use()\`.`
`params are being enumerated incompletely missing these properties: ${describedMissingProperties}. ` +
`\`params\` should be unwrapped with \`React.use()\` before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
} else {
console.error(
`params are being enumerated with \`{...params}\`, \`Object.keys(params)\`, or similar. \`params\` is now a Promise, however in the current version of Next.js direct access to the underlying params object is still supported to facilitate migration to the new type. You should update your code to unwrap \`params\` with \`React.use()\` before accessing its properties.`
`params are being enumerated. ` +
`\`params\` should be unwrapped with \`React.use()\` before using its value. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}
}
Expand Down
12 changes: 9 additions & 3 deletions packages/next/src/server/request/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,9 @@ function makeDynamicallyTrackedExoticParamsWithDevWarnings(
function warnForSyncAccess(route: undefined | string, expression: string) {
const prefix = route ? ` In route ${route} a ` : 'A '
console.error(
`${prefix}param property was accessed directly with ${expression}. \`params\` is now a Promise and should be awaited before accessing properties of the underlying params object. In this version of Next.js direct access to param properties is still supported to facilitate migration but in a future version you will be required to await \`params\`. If this use is inside an async function await it. If this use is inside a synchronous function then convert the function to async or await it from outside this function and pass the result in.`
`${prefix}param property was accessed directly with ${expression}. ` +
`\`params\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}

Expand All @@ -511,11 +513,15 @@ function warnForEnumeration(
const describedMissingProperties =
describeListOfPropertyNames(missingProperties)
console.error(
`${prefix}params are being enumerated incompletely with \`{...params}\`, \`Object.keys(params)\`, or similar. The following properties were not copied: ${describedMissingProperties}. \`params\` is now a Promise, however in the current version of Next.js direct access to the underlying params object is still supported to facilitate migration to the new type. param names that conflict with Promise properties cannot be accessed directly and must be accessed by first awaiting the \`params\` promise.`
`${prefix}params are being enumerated incompletely missing these properties: ${describedMissingProperties}. ` +
`\`params\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
} else {
console.error(
`${prefix}params are being enumerated with \`{...params}\`, \`Object.keys(params)\`, or similar. \`params\` is now a Promise, however in the current version of Next.js direct access to the underlying params object is still supported to facilitate migration to the new type. You should update your code to await \`params\` before accessing its properties.`
`${prefix}params are being enumerated. ` +
`\`params\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}
}
Expand Down
8 changes: 6 additions & 2 deletions packages/next/src/server/request/search-params.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,16 @@ function makeUntrackedExoticSearchParams(

function warnForSyncAccess(expression: string) {
console.error(
`A searchParam property was accessed directly with ${expression}. \`searchParams\` is now a Promise and should be unwrapped with \`React.use()\` before accessing properties of the underlying searchParams object. In this version of Next.js direct access to searchParam properties is still supported to facilitate migration but in a future version you will be required to unwrap \`searchParams\` with \`React.use()\`.`
`A searchParam property was accessed directly with ${expression}. ` +
`\`searchParams\` should be unwrapped with \`React.use()\` before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}

function warnForSyncSpread() {
console.error(
`the keys of \`searchParams\` were accessed through something like \`Object.keys(searchParams)\` or \`{...searchParams}\`. \`searchParams\` is now a Promise and should be unwrapped with \`React.use()\` in Client Components before accessing properties of the underlying searchParams object. In this version of Next.js direct access to searchParam properties is still supported to facilitate migration but in a future version you will be required to unwrap \`searchParams\` with \`React.use()\`.`
`The keys of \`searchParams\` were accessed directly. ` +
`\`searchParams\` should be unwrapped with \`React.use()\` before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}
12 changes: 9 additions & 3 deletions packages/next/src/server/request/search-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,9 @@ function makeDynamicallyTrackedExoticSearchParamsWithDevWarnings(
function warnForSyncAccess(route: undefined | string, expression: string) {
const prefix = route ? ` In route ${route} a ` : 'A '
console.error(
`${prefix}searchParam property was accessed directly with ${expression}. \`searchParams\` is now a Promise and should be awaited before accessing properties of the underlying searchParams object. In this version of Next.js direct access to searchParam properties is still supported to facilitate migration but in a future version you will be required to await \`searchParams\`. If this use is inside an async function await it. If this use is inside a synchronous function then convert the function to async or await it from outside this function and pass the result in.`
`${prefix}searchParam property was accessed directly with ${expression}. ` +
`\`searchParams\` should be awaited before accessing properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}

Expand All @@ -673,11 +675,15 @@ function warnForEnumeration(
const describedMissingProperties =
describeListOfPropertyNames(missingProperties)
console.error(
`${prefix}searchParams are being enumerated incompletely with \`{...searchParams}\`, \`Object.keys(searchParams)\`, or similar. The following properties were not copied: ${describedMissingProperties}. \`searchParams\` is now a Promise, however in the current version of Next.js direct access to the underlying searchParams object is still supported to facilitate migration to the new type. search parameter names that conflict with Promise properties cannot be accessed directly and must be accessed by first awaiting the \`searchParams\` promise.`
`${prefix}searchParams are being enumerated incompletely missing these properties: ${describedMissingProperties}. ` +
`\`searchParams\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
} else {
console.error(
`${prefix}searchParams are being enumerated with \`{...searchParams}\`, \`Object.keys(searchParams)\`, or similar. \`searchParams\` is now a Promise, however in the current version of Next.js direct access to the underlying searchParams object is still supported to facilitate migration to the new type. You should update your code to await \`searchParams\` before accessing its properties.`
`${prefix}searchParams are being enumerated. ` +
`\`searchParams\` should be awaited before accessing its properties. ` +
`Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis`
)
}
}
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/app-dir/dynamic-io/dynamic-io.cookies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ describe('dynamic-io', () => {
)
expect($('#for-of-x-sentinel-rand').text()).toContain('x-sentinel-rand')
if (isNextDev) {
expect(cookieWarnings[i++]).toContain('for...of cookies()')
expect(cookieWarnings[i++]).toContain('cookies were iterated over')
}

// ...spread iteration
Expand All @@ -211,7 +211,7 @@ describe('dynamic-io', () => {
)
expect($('#spread-x-sentinel-rand').text()).toContain('x-sentinel-rand')
if (isNextDev) {
expect(cookieWarnings[i++]).toContain('[...cookies()]')
expect(cookieWarnings[i++]).toContain('cookies were iterated over')
}

// cookies().size
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/app-dir/dynamic-io/dynamic-io.headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ describe('dynamic-io', () => {
)
expect($('#for-of-x-sentinel-rand').length).toBe(1)
if (isNextDev) {
expect(headerWarnings[i++]).toContain('for...of headers()')
expect(headerWarnings[i++]).toContain('headers were iterated over.')
}

// ...headers()
Expand All @@ -324,7 +324,7 @@ describe('dynamic-io', () => {
)
expect($('#spread-x-sentinel-rand').length).toBe(1)
if (isNextDev) {
expect(headerWarnings[i++]).toContain('...headers()')
expect(headerWarnings[i++]).toContain('headers were iterated over.')
}

if (isNextDev) {
Expand Down
Loading

0 comments on commit 772b0ea

Please sign in to comment.