Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Breaking] Update Dynamic APIs to be async (vercel#68812)
Next.js has a number of dynamic APIs that aren't available during prerendering. What happens when you access these APIs might differ depending on the mode you are using, for instance if you have PPR turned on or the newly introduced `dynamicIO` experimental mode. But regardless of the mode the underlying API represents accessing something that might only be available at render time (dynamic rendering) rather than prerender time (build and revalidate rendering) Unfortunately our current dynamic APIs make certain kinds of modeling tricky because they are all synchronous. For instance if we wanted to add a feature to Next.js where we started a dynamic render before a Request even hits the server it would be interesting to be able to start working on everything that does not rely on any dynamic data and then once a real Request arrives we can continue the render and provide the associated Request context through our dynamic APIs. If our dynamic APIs were all async we could build something like this because they represnt a value that will eventually resolve to some Request value. This PR updates most existing dynamic APIs to be async rather than sync. This is a breaking change and will need to be paired with codemods to realistically adopt. Additionally since this change is so invasive I have implemented it in a way to maximize backward compatibility by still allowing most synchronous access. The combination of codemods, typescript updates, and backward compat functionality should make it possible for projects to upgrade to the latest version with minimal effort and then follow up with a complete conversion over time. #### `cookies()` `cookies()` now returns `Promise<ReadonlyRequestCookies>`. Synchronous access to the underlying RequestCookies object is still supported to facilitate migration. ```tsx // ------------ preferred usage // async Server Component const token = (await cookies()).get('token') // sync Server Component import { use } from 'react' //... const token = use(cookies()).get('token') // ------------ temporarily allowed usage // javascript, dev warning at runtime const token = cookies().get('token') // typescript, dev warning at runtime import { type UnsafeUnwrappedCookies } from 'next/headers' // ... const token = (cookies() as unknown as UnsafeUnwrappedCookies).get('token') ``` #### `headers()` `headers()` now returns `Promise<ReadonlyHeaders>`. Synchronous access to the underlying Headers object is still supported to facilitate migration. ```tsx // ------------ preferred usage // async Server Component const header = (await headers()).get('x-foo') // sync Server Component import { use } from 'react' //... const header = use(headers()).get('x-foo') // ------------ temporarily allowed usage // javascript, dev warning at runtime const header = headers().get('x-foo') // typescript, dev warning at runtime import { type UnsafeUnwrappedHeaders } from 'next/headers' // ... const header = (headers() as unknown as UnsafeUnwrappedHeaders).get('x-foo') ``` #### `draftMode()` `draftMode()` now returns `Promise<DraftMode>`. Synchronous access to the underlying DraftMode object is still supported to facilitate migration. ```tsx // ------------ preferred usage // async Server Component if ((await draftMode()).isEnabled) { ... } // sync Server Component import { use } from 'react' //... if (use(draftMode()).isEnabled) { ... } // ------------ temporarily allowed usage // javascript, dev warning at runtime if (draftMode().isEnabled) { ... } // typescript, dev warning at runtime import { type UnsafeUnwrappedDraftMode} from 'next/headers' // ... if ((draftMode() as unknown as UnsafeUnwrappedDraftMode).isEnabled) { ... } ``` #### `searchParams` `searchParams` is now a `Promise<{...}>`. Synchronous access to the underlying search params is still supported to facilitate migration. ```tsx // ------------ preferred usage // async Page Component export default async function Page({ searchParams }: { searchParams: Promise<{ foo: string }> }) { const fooSearchParam = (await searchParams).foo } // sync Page Component import { use } from 'react' export default function Page({ searchParams }: { searchParams: Promise<{ foo: string }> }) { const fooSearchParam = use(searchParams).foo } // ------------ temporarily allowed usage // javascript, dev warning at runtime export default async function Page({ searchParams}) { const fooSearchParam = searchParams.foo } // typescript, dev warning at runtime import { type UnsafeUnwrappedSearchParams } from 'next/server' export default async function Page({ searchParams }: { searchParams: Promise<{ foo: string }> }) { const syncSearchParams = (searchParams as unknown as UnsafeUnwrappedSearchParams<typeof searchParams>) const fooSearchParam = syncSearchParams.foo } ``` #### `params` `params` is now a `Promise<{...}>`. Synchronous access to the underlying params is still supported to facilitate migration. It should be noted that while params are not usually dynamic there are certain modes where they can be such as fallback prerenders for PPR. ```tsx // ------------ preferred usage // async Segment Component export default async function Layout({ params }: { params: Promise<{ foo: string }> }) { const fooParam = (await params).foo } // sync Segment Component import { use } from 'react' export default function Layout({ params }: { params: Promise<{ foo: string }> }) { const fooParam = use(params).foo } // ------------ temporarily allowed usage // javascript, dev warning at runtime export default async function Layout({ params}) { const fooParam = params.foo } // typescript, dev warning at runtime import { type UnsafeUnwrappedParams } from 'next/headers' export default async function Layout({ params }: { params: Promise<{ foo: string }> }) { const syncParams = (params as unknown as UnsafeUnwrappedParams<typeof params>) const fooSearchParam = syncParams.foo } ``` ### Typescript Changes When using typescript with Next.js currently it is up to you to author types for Pages, Layouts and other Segment components that recieve props like `params` and `searchParams`. Next comes with some build-time type checking to ensure you have not improperly typed various top level module exports however the current type assertions for `params` and `searchParams` is `any`. This isn't very helpful because it allows you to erroneously type these props. `searchParams` is tricky because while the default type is a dictionary object parsed using node.js url parsing it is possible to customize when running a custom Next.js server. However we can ensure that you correctly type the prop as a Promise so with this change the validated type for `searchParams` will be `Promise<any>`. In the long run we will look at updating the `searchParams` underlying type to be URLSearchParams so we can move away from supporting customized parsing during rendering and we can get even more explicit about valid types. `params` is more straight forward because the framework controls the actual `params` prop implementation and no customization is possible. In the long run we want to enforce you are only typing params that are valid for the Layout level your file is located in but for now we are updating the allowed type to be `Promise<{[key: string]: string | string[] | undefined }>`. These new type restrictions may also require fixes before being able to successfully build a project that updates to include these breaking changes. These changes will also not always be codemodable because it is valid to type the entire component using an opaque type like `Props` which our codemods may not have an ability to introspect or modify.
- Loading branch information