Skip to content

Commit f6303cc

Browse files
gnoffabhi12299
authored andcommitted
[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.
1 parent 69188a9 commit f6303cc

File tree

347 files changed

+11746
-1014
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

347 files changed

+11746
-1014
lines changed

.vscode/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,29 @@
6060
"codemod",
6161
"codemods",
6262
"Destructuring",
63+
"buildtime",
64+
"callsites",
65+
"codemod",
66+
"datastream",
67+
"deduped",
68+
"draftmode",
6369
"Entrypoints",
6470
"jscodeshift",
6571
"napi",
72+
"navigations",
6673
"nextjs",
6774
"opentelemetry",
75+
"Preinit",
6876
"prerendered",
77+
"prerendering",
78+
"proxied",
79+
"renderable",
80+
"revalidates",
81+
"subresource",
82+
"thenables",
6983
"Threadsafe",
7084
"Turbopack",
85+
"unproxied",
7186
"zipkin"
7287
],
7388
"grammarly.selectors": [

packages/next/headers.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
export * from './dist/server/request/cookies'
12
export * from './dist/server/request/headers'
3+
export * from './dist/server/request/draft-mode'

packages/next/headers.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
module.exports = require('./dist/server/request/headers')
1+
module.exports.cookies = require('./dist/server/request/cookies').cookies
2+
module.exports.headers = require('./dist/server/request/headers').headers
3+
module.exports.draftMode = require('./dist/server/request/draft-mode').draftMode

packages/next/server.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
1414
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
1515
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
1616
export { unstable_after } from 'next/dist/server/after'
17+
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
18+
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'

packages/next/src/api/headers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
export * from '../server/request/cookies'
12
export * from '../server/request/headers'
3+
export * from '../server/request/draft-mode'

packages/next/src/build/webpack/plugins/next-types-plugin/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ checkFields<Diff<{
8585
}
8686
}, TEntry, ''>>()
8787
88+
${options.type === 'route' ? `type RouteContext = { params: Promise<SegmentParams> }` : ''}
8889
${
8990
options.type === 'route'
9091
? HTTP_METHODS.map(
@@ -103,7 +104,7 @@ if ('${method}' in entry) {
103104
>()
104105
checkFields<
105106
Diff<
106-
ParamCheck<PageParams>,
107+
ParamCheck<RouteContext>,
107108
{
108109
__tag__: '${method}'
109110
__param_position__: 'second'
@@ -158,14 +159,14 @@ if ('generateViewport' in entry) {
158159
}
159160
// Check the arguments and return type of the generateStaticParams function
160161
if ('generateStaticParams' in entry) {
161-
checkFields<Diff<{ params: PageParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
162+
checkFields<Diff<{ params: SegmentParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
162163
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
163164
}
164165
165-
type PageParams = any
166+
type SegmentParams = {[param: string]: string | string[] | undefined}
166167
export interface PageProps {
167-
params?: any
168-
searchParams?: any
168+
params?: Promise<SegmentParams>
169+
searchParams?: Promise<any>
169170
}
170171
export interface LayoutProps {
171172
children?: React.ReactNode
@@ -174,7 +175,7 @@ ${
174175
? options.slots.map((slot) => ` ${slot}: React.ReactNode`).join('\n')
175176
: ''
176177
}
177-
params?: any
178+
params?: Promise<SegmentParams>
178179
}
179180
180181
// =============
Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,76 @@
11
'use client'
22

3+
import type { ParsedUrlQuery } from 'querystring'
4+
import { InvariantError } from '../../shared/lib/invariant-error'
5+
6+
import type { Params } from '../../server/request/params'
7+
8+
/**
9+
* When the Page is a client component we send the params and searchParams to this client wrapper
10+
* where they are turned into dynamically tracked values before being passed to the actual Page component.
11+
*
12+
* additionally we may send promises representing the params and searchParams. We don't ever use these passed
13+
* values but it can be necessary for the sender to send a Promise that doesn't resolve in certain situations.
14+
* It is up to the caller to decide if the promises are needed.
15+
*/
316
export function ClientPageRoot({
417
Component,
5-
props,
18+
searchParams,
19+
params,
20+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
21+
promises,
622
}: {
723
Component: React.ComponentType<any>
8-
props: { [props: string]: any }
24+
searchParams: ParsedUrlQuery
25+
params: Params
26+
promises?: Array<Promise<any>>
927
}) {
1028
if (typeof window === 'undefined') {
11-
const { createDynamicallyTrackedParams } =
12-
require('../../server/request/fallback-params') as typeof import('../../server/request/fallback-params')
13-
const { createDynamicallyTrackedSearchParams } =
14-
require('../../server/request/search-params') as typeof import('../../server/request/search-params')
15-
16-
// We expect to be passed searchParams but even if we aren't we can construct one from
17-
// an empty object. We only do this if we are in a static generation as a performance
18-
// optimization. Ideally we'd unconditionally construct the tracked params but since
19-
// this creates a proxy which is slow and this would happen even for client navigations
20-
// that are done entirely dynamically and we know there the dynamic tracking is a noop
21-
// in this dynamic case we can safely elide it.
22-
props.searchParams = createDynamicallyTrackedSearchParams(
23-
props.searchParams || {}
24-
)
25-
props.params = props.params
26-
? createDynamicallyTrackedParams(props.params)
27-
: {}
29+
const { staticGenerationAsyncStorage } =
30+
require('./static-generation-async-storage.external') as typeof import('./static-generation-async-storage.external')
31+
32+
let clientSearchParams: Promise<ParsedUrlQuery>
33+
let clientParams: Promise<Params>
34+
// We are going to instrument the searchParams prop with tracking for the
35+
// appropriate context. We wrap differently in prerendering vs rendering
36+
const store = staticGenerationAsyncStorage.getStore()
37+
if (!store) {
38+
throw new InvariantError(
39+
'Expected staticGenerationStore to exist when handling searchParams in a client Page.'
40+
)
41+
}
42+
43+
if (store.isStaticGeneration) {
44+
// We are in a prerender context
45+
const { createPrerenderSearchParamsFromClient } =
46+
require('../../server/request/search-params') as typeof import('../../server/request/search-params')
47+
clientSearchParams = createPrerenderSearchParamsFromClient(store)
48+
49+
const { createPrerenderParamsFromClient } =
50+
require('../../server/request/params') as typeof import('../../server/request/params')
51+
52+
clientParams = createPrerenderParamsFromClient(params, store)
53+
} else {
54+
const { createRenderSearchParamsFromClient } =
55+
require('../../server/request/search-params') as typeof import('../../server/request/search-params')
56+
clientSearchParams = createRenderSearchParamsFromClient(
57+
searchParams,
58+
store
59+
)
60+
const { createRenderParamsFromClient } =
61+
require('../../server/request/params') as typeof import('../../server/request/params')
62+
clientParams = createRenderParamsFromClient(params, store)
63+
}
64+
65+
return <Component params={clientParams} searchParams={clientSearchParams} />
66+
} else {
67+
const { createRenderSearchParamsFromClient } =
68+
require('../../server/request/search-params.browser') as typeof import('../../server/request/search-params.browser')
69+
const clientSearchParams = createRenderSearchParamsFromClient(searchParams)
70+
const { createRenderParamsFromClient } =
71+
require('../../server/request/params.browser') as typeof import('../../server/request/params.browser')
72+
const clientParams = createRenderParamsFromClient(params)
73+
74+
return <Component params={clientParams} searchParams={clientSearchParams} />
2875
}
29-
return <Component {...props} />
3076
}
Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,58 @@
11
'use client'
22

3-
type ClientSegmentRootProps = {
4-
Component: React.ComponentType
5-
props: { [props: string]: any }
6-
}
3+
import { InvariantError } from '../../shared/lib/invariant-error'
4+
5+
import type { Params } from '../../server/request/params'
76

7+
/**
8+
* When the Page is a client component we send the params to this client wrapper
9+
* where they are turned into dynamically tracked values before being passed to the actual Segment component.
10+
*
11+
* additionally we may send a promise representing params. We don't ever use this passed
12+
* value but it can be necessary for the sender to send a Promise that doesn't resolve in certain situations
13+
* such as when dynamicIO is enabled. It is up to the caller to decide if the promises are needed.
14+
*/
815
export function ClientSegmentRoot({
916
Component,
10-
props,
11-
}: ClientSegmentRootProps) {
17+
slots,
18+
params,
19+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
20+
promise,
21+
}: {
22+
Component: React.ComponentType<any>
23+
slots: { [key: string]: React.ReactNode }
24+
params: Params
25+
promise?: Promise<any>
26+
}) {
1227
if (typeof window === 'undefined') {
13-
const { createDynamicallyTrackedParams } =
14-
require('../../server/request/fallback-params') as typeof import('../../server/request/fallback-params')
28+
const { staticGenerationAsyncStorage } =
29+
require('./static-generation-async-storage.external') as typeof import('./static-generation-async-storage.external')
30+
31+
let clientParams: Promise<Params>
32+
// We are going to instrument the searchParams prop with tracking for the
33+
// appropriate context. We wrap differently in prerendering vs rendering
34+
const store = staticGenerationAsyncStorage.getStore()
35+
if (!store) {
36+
throw new InvariantError(
37+
'Expected staticGenerationStore to exist when handling params in a client segment such as a Layout or Template.'
38+
)
39+
}
40+
41+
const { createPrerenderParamsFromClient } =
42+
require('../../server/request/params') as typeof import('../../server/request/params')
1543

16-
props.params = props.params
17-
? createDynamicallyTrackedParams(props.params)
18-
: {}
44+
if (store.isStaticGeneration) {
45+
clientParams = createPrerenderParamsFromClient(params, store)
46+
} else {
47+
const { createRenderParamsFromClient } =
48+
require('../../server/request/params') as typeof import('../../server/request/params')
49+
clientParams = createRenderParamsFromClient(params, store)
50+
}
51+
return <Component {...slots} params={clientParams} />
52+
} else {
53+
const { createRenderParamsFromClient } =
54+
require('../../server/request/params.browser') as typeof import('../../server/request/params.browser')
55+
const clientParams = createRenderParamsFromClient(params)
56+
return <Component {...slots} params={clientParams} />
1957
}
20-
return <Component {...props} />
2158
}

packages/next/src/client/components/navigation.ts

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { getSegmentValue } from './router-reducer/reducers/get-segment-value'
1616
import { PAGE_SEGMENT_KEY, DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
1717
import { ReadonlyURLSearchParams } from './navigation.react-server'
18-
import { trackFallbackParamAccessed } from '../../server/app-render/dynamic-rendering'
18+
import { useDynamicRouteParams } from '../../server/app-render/dynamic-rendering'
1919

2020
/**
2121
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
@@ -65,27 +65,6 @@ export function useSearchParams(): ReadonlyURLSearchParams {
6565
return readonlySearchParams
6666
}
6767

68-
function trackParamsAccessed(expression: string) {
69-
if (typeof window === 'undefined') {
70-
// AsyncLocalStorage should not be included in the client bundle.
71-
const { staticGenerationAsyncStorage } =
72-
require('./static-generation-async-storage.external') as typeof import('./static-generation-async-storage.external')
73-
74-
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
75-
76-
if (
77-
staticGenerationStore &&
78-
staticGenerationStore.isStaticGeneration &&
79-
staticGenerationStore.fallbackRouteParams &&
80-
staticGenerationStore.fallbackRouteParams.size > 0
81-
) {
82-
// There are fallback route params, we should track these as dynamic
83-
// accesses.
84-
trackFallbackParamAccessed(staticGenerationStore, expression)
85-
}
86-
}
87-
}
88-
8968
/**
9069
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
9170
* that lets you read the current URL's pathname.
@@ -105,7 +84,7 @@ function trackParamsAccessed(expression: string) {
10584
*/
10685
// Client components API
10786
export function usePathname(): string {
108-
trackParamsAccessed('usePathname()')
87+
useDynamicRouteParams('usePathname()')
10988

11089
// In the case where this is `null`, the compat types added in `next-env.d.ts`
11190
// will add a new overload that changes the return type to include `null`.
@@ -165,21 +144,19 @@ export function useRouter(): AppRouterInstance {
165144
*/
166145
// Client components API
167146
export function useParams<T extends Params = Params>(): T {
168-
trackParamsAccessed('useParams()')
147+
useDynamicRouteParams('useParams()')
169148

170149
return useContext(PathParamsContext) as T
171150
}
172151

173152
/** Get the canonical parameters from the current level to the leaf node. */
174153
// Client components API
175-
export function getSelectedLayoutSegmentPath(
154+
function getSelectedLayoutSegmentPath(
176155
tree: FlightRouterState,
177156
parallelRouteKey: string,
178157
first = true,
179158
segmentPath: string[] = []
180159
): string[] {
181-
trackParamsAccessed('getSelectedLayoutSegmentPath()')
182-
183160
let node: FlightRouterState
184161
if (first) {
185162
// Use the provided parallel route key on the first parallel route
@@ -238,7 +215,7 @@ export function getSelectedLayoutSegmentPath(
238215
export function useSelectedLayoutSegments(
239216
parallelRouteKey: string = 'children'
240217
): string[] {
241-
trackParamsAccessed('useSelectedLayoutSegments()')
218+
useDynamicRouteParams('useSelectedLayoutSegments()')
242219

243220
const context = useContext(LayoutRouterContext)
244221
// @ts-expect-error This only happens in `pages`. Type is overwritten in navigation.d.ts
@@ -269,7 +246,7 @@ export function useSelectedLayoutSegments(
269246
export function useSelectedLayoutSegment(
270247
parallelRouteKey: string = 'children'
271248
): string | null {
272-
trackParamsAccessed('useSelectedLayoutSegment()')
249+
useDynamicRouteParams('useSelectedLayoutSegment()')
273250

274251
const selectedLayoutSegments = useSelectedLayoutSegments(parallelRouteKey)
275252

0 commit comments

Comments
 (0)