Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Breaking] Update Dynamic APIs to be async #68812

Merged
merged 14 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 15 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,29 @@
},
"cSpell.words": [
"Destructuring",
"buildtime",
"callsites",
"codemod",
"datastream",
"deduped",
"draftmode",
"Entrypoints",
"jscodeshift",
"napi",
"navigations",
"nextjs",
"opentelemetry",
"Preinit",
"prerendered",
"prerendering",
"proxied",
"renderable",
"revalidates",
"subresource",
"thenables",
"Threadsafe",
"Turbopack",
"unproxied",
"zipkin"
],
"grammarly.selectors": [
Expand Down
2 changes: 2 additions & 0 deletions packages/next/headers.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './dist/server/request/cookies'
export * from './dist/server/request/headers'
export * from './dist/server/request/draft-mode'
4 changes: 3 additions & 1 deletion packages/next/headers.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
module.exports = require('./dist/server/request/headers')
module.exports.cookies = require('./dist/server/request/cookies').cookies
module.exports.headers = require('./dist/server/request/headers').headers
module.exports.draftMode = require('./dist/server/request/draft-mode').draftMode
2 changes: 2 additions & 0 deletions packages/next/server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export { URLPattern } from 'next/dist/compiled/@edge-runtime/primitives/url'
export { ImageResponse } from 'next/dist/server/web/spec-extension/image-response'
export type { ImageResponseOptions } from 'next/dist/compiled/@vercel/og/types'
export { unstable_after } from 'next/dist/server/after'
export type { UnsafeUnwrappedSearchParams } from 'next/dist/server/request/search-params'
export type { UnsafeUnwrappedParams } from 'next/dist/server/request/params'
2 changes: 2 additions & 0 deletions packages/next/src/api/headers.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from '../server/request/cookies'
export * from '../server/request/headers'
export * from '../server/request/draft-mode'
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ checkFields<Diff<{
}
}, TEntry, ''>>()

${options.type === 'route' ? `type RouteContext = { params: Promise<SegmentParams> }` : ''}
${
options.type === 'route'
? HTTP_METHODS.map(
Expand All @@ -103,7 +104,7 @@ if ('${method}' in entry) {
>()
checkFields<
Diff<
ParamCheck<PageParams>,
ParamCheck<RouteContext>,
{
__tag__: '${method}'
__param_position__: 'second'
Expand Down Expand Up @@ -158,14 +159,14 @@ if ('generateViewport' in entry) {
}
// Check the arguments and return type of the generateStaticParams function
if ('generateStaticParams' in entry) {
checkFields<Diff<{ params: PageParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
checkFields<Diff<{ params: SegmentParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
}

type PageParams = any
type SegmentParams = {[param: string]: string | string[] | undefined}
gnoff marked this conversation as resolved.
Show resolved Hide resolved
export interface PageProps {
params?: any
searchParams?: any
params?: Promise<SegmentParams>
searchParams?: Promise<any>
}
export interface LayoutProps {
children?: React.ReactNode
Expand All @@ -174,7 +175,7 @@ ${
? options.slots.map((slot) => ` ${slot}: React.ReactNode`).join('\n')
: ''
}
params?: any
params?: Promise<SegmentParams>
}

// =============
Expand Down
86 changes: 66 additions & 20 deletions packages/next/src/client/components/client-page.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,76 @@
'use client'

import type { ParsedUrlQuery } from 'querystring'
import { InvariantError } from '../../shared/lib/invariant-error'

import type { Params } from '../../server/request/params'

/**
* When the Page is a client component we send the params and searchParams to this client wrapper
* where they are turned into dynamically tracked values before being passed to the actual Page component.
*
* additionally we may send promises representing the params and searchParams. We don't ever use these passed
* values but it can be necessary for the sender to send a Promise that doesn't resolve in certain situations.
* It is up to the caller to decide if the promises are needed.
*/
export function ClientPageRoot({
Component,
props,
searchParams,
params,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
promises,
gnoff marked this conversation as resolved.
Show resolved Hide resolved
}: {
Component: React.ComponentType<any>
props: { [props: string]: any }
searchParams: ParsedUrlQuery
params: Params
promises?: Array<Promise<any>>
}) {
if (typeof window === 'undefined') {
const { createDynamicallyTrackedParams } =
require('../../server/request/fallback-params') as typeof import('../../server/request/fallback-params')
const { createDynamicallyTrackedSearchParams } =
require('../../server/request/search-params') as typeof import('../../server/request/search-params')

// We expect to be passed searchParams but even if we aren't we can construct one from
// an empty object. We only do this if we are in a static generation as a performance
// optimization. Ideally we'd unconditionally construct the tracked params but since
// this creates a proxy which is slow and this would happen even for client navigations
// that are done entirely dynamically and we know there the dynamic tracking is a noop
// in this dynamic case we can safely elide it.
props.searchParams = createDynamicallyTrackedSearchParams(
props.searchParams || {}
)
props.params = props.params
? createDynamicallyTrackedParams(props.params)
: {}
const { staticGenerationAsyncStorage } =
require('./static-generation-async-storage.external') as typeof import('./static-generation-async-storage.external')

let clientSearchParams: Promise<ParsedUrlQuery>
let clientParams: Promise<Params>
// We are going to instrument the searchParams prop with tracking for the
// appropriate context. We wrap differently in prerendering vs rendering
const store = staticGenerationAsyncStorage.getStore()
if (!store) {
throw new InvariantError(
'Expected staticGenerationStore to exist when handling searchParams in a client Page.'
)
}

if (store.isStaticGeneration) {
// We are in a prerender context
const { createPrerenderSearchParamsFromClient } =
require('../../server/request/search-params') as typeof import('../../server/request/search-params')
clientSearchParams = createPrerenderSearchParamsFromClient(store)

const { createPrerenderParamsFromClient } =
require('../../server/request/params') as typeof import('../../server/request/params')

clientParams = createPrerenderParamsFromClient(params, store)
} else {
const { createRenderSearchParamsFromClient } =
require('../../server/request/search-params') as typeof import('../../server/request/search-params')
clientSearchParams = createRenderSearchParamsFromClient(
searchParams,
store
)
const { createRenderParamsFromClient } =
require('../../server/request/params') as typeof import('../../server/request/params')
clientParams = createRenderParamsFromClient(params, store)
}

return <Component params={clientParams} searchParams={clientSearchParams} />
} else {
const { createRenderSearchParamsFromClient } =
require('../../server/request/search-params.browser') as typeof import('../../server/request/search-params.browser')
huozhi marked this conversation as resolved.
Show resolved Hide resolved
const clientSearchParams = createRenderSearchParamsFromClient(searchParams)
const { createRenderParamsFromClient } =
require('../../server/request/params.browser') as typeof import('../../server/request/params.browser')
const clientParams = createRenderParamsFromClient(params)

return <Component params={clientParams} searchParams={clientSearchParams} />
}
return <Component {...props} />
}
61 changes: 49 additions & 12 deletions packages/next/src/client/components/client-segment.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,58 @@
'use client'

type ClientSegmentRootProps = {
Component: React.ComponentType
props: { [props: string]: any }
}
import { InvariantError } from '../../shared/lib/invariant-error'

import type { Params } from '../../server/request/params'

/**
* When the Page is a client component we send the params to this client wrapper
* where they are turned into dynamically tracked values before being passed to the actual Segment component.
*
* additionally we may send a promise representing params. We don't ever use this passed
* value but it can be necessary for the sender to send a Promise that doesn't resolve in certain situations
* such as when dynamicIO is enabled. It is up to the caller to decide if the promises are needed.
*/
export function ClientSegmentRoot({
Component,
props,
}: ClientSegmentRootProps) {
slots,
params,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
promise,
}: {
Component: React.ComponentType<any>
slots: { [key: string]: React.ReactNode }
params: Params
promise?: Promise<any>
}) {
if (typeof window === 'undefined') {
const { createDynamicallyTrackedParams } =
require('../../server/request/fallback-params') as typeof import('../../server/request/fallback-params')
const { staticGenerationAsyncStorage } =
require('./static-generation-async-storage.external') as typeof import('./static-generation-async-storage.external')

let clientParams: Promise<Params>
// We are going to instrument the searchParams prop with tracking for the
// appropriate context. We wrap differently in prerendering vs rendering
const store = staticGenerationAsyncStorage.getStore()
if (!store) {
throw new InvariantError(
'Expected staticGenerationStore to exist when handling params in a client segment such as a Layout or Template.'
)
}

const { createPrerenderParamsFromClient } =
require('../../server/request/params') as typeof import('../../server/request/params')

props.params = props.params
? createDynamicallyTrackedParams(props.params)
: {}
if (store.isStaticGeneration) {
clientParams = createPrerenderParamsFromClient(params, store)
} else {
const { createRenderParamsFromClient } =
require('../../server/request/params') as typeof import('../../server/request/params')
clientParams = createRenderParamsFromClient(params, store)
}
return <Component {...slots} params={clientParams} />
} else {
const { createRenderParamsFromClient } =
require('../../server/request/params.browser') as typeof import('../../server/request/params.browser')
const clientParams = createRenderParamsFromClient(params)
return <Component {...slots} params={clientParams} />
}
return <Component {...props} />
}
35 changes: 6 additions & 29 deletions packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { getSegmentValue } from './router-reducer/reducers/get-segment-value'
import { PAGE_SEGMENT_KEY, DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
import { ReadonlyURLSearchParams } from './navigation.react-server'
import { trackFallbackParamAccessed } from '../../server/app-render/dynamic-rendering'
import { useDynamicRouteParams } from '../../server/app-render/dynamic-rendering'

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

function trackParamsAccessed(expression: string) {
if (typeof window === 'undefined') {
// AsyncLocalStorage should not be included in the client bundle.
const { staticGenerationAsyncStorage } =
require('./static-generation-async-storage.external') as typeof import('./static-generation-async-storage.external')

const staticGenerationStore = staticGenerationAsyncStorage.getStore()

if (
staticGenerationStore &&
staticGenerationStore.isStaticGeneration &&
staticGenerationStore.fallbackRouteParams &&
staticGenerationStore.fallbackRouteParams.size > 0
) {
// There are fallback route params, we should track these as dynamic
// accesses.
trackFallbackParamAccessed(staticGenerationStore, expression)
}
}
}

/**
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
* that lets you read the current URL's pathname.
Expand All @@ -105,7 +84,7 @@ function trackParamsAccessed(expression: string) {
*/
// Client components API
export function usePathname(): string {
trackParamsAccessed('usePathname()')
useDynamicRouteParams('usePathname()')

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

return useContext(PathParamsContext) as T
}

/** Get the canonical parameters from the current level to the leaf node. */
// Client components API
export function getSelectedLayoutSegmentPath(
function getSelectedLayoutSegmentPath(
tree: FlightRouterState,
parallelRouteKey: string,
first = true,
segmentPath: string[] = []
): string[] {
trackParamsAccessed('getSelectedLayoutSegmentPath()')

let node: FlightRouterState
if (first) {
// Use the provided parallel route key on the first parallel route
Expand Down Expand Up @@ -238,7 +215,7 @@ export function getSelectedLayoutSegmentPath(
export function useSelectedLayoutSegments(
parallelRouteKey: string = 'children'
): string[] {
trackParamsAccessed('useSelectedLayoutSegments()')
useDynamicRouteParams('useSelectedLayoutSegments()')

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

const selectedLayoutSegments = useSelectedLayoutSegments(parallelRouteKey)

Expand Down
Loading
Loading