-
Notifications
You must be signed in to change notification settings - Fork 27k
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
Dynamic APIs #60645
Dynamic APIs #60645
Changes from all commits
e66ae43
b8f40bc
d59e4b8
2324e67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache' | ||
export { revalidatePath } from 'next/dist/server/web/spec-extension/revalidate-path' | ||
export { revalidateTag } from 'next/dist/server/web/spec-extension/revalidate-tag' | ||
export { | ||
revalidateTag, | ||
revalidatePath, | ||
} from 'next/dist/server/web/spec-extension/revalidate' | ||
export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
'use client' | ||
import { createDynamicallyTrackedSearchParams } from './search-params' | ||
|
||
export function ClientPageRoot({ | ||
Component, | ||
props, | ||
}: { | ||
Component: React.ComponentType<any> | ||
props: { [props: string]: any } | ||
}) { | ||
// 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 || {} | ||
) | ||
return <Component {...props} /> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import type { ParsedUrlQuery } from 'querystring' | ||
|
||
import { staticGenerationAsyncStorage } from './static-generation-async-storage.external' | ||
import { trackDynamicDataAccessed } from '../../server/app-render/dynamic-rendering' | ||
import { ReflectAdapter } from '../../server/web/spec-extension/adapters/reflect' | ||
|
||
/** | ||
* Takes a ParsedUrlQuery object and either returns it unmodified or returns an empty object | ||
* | ||
* Even though we do not track read access on the returned searchParams we need to | ||
* return an empty object if we are doing a 'force-static' render. This is to ensure | ||
* we don't encode the searchParams into the flight data. | ||
*/ | ||
export function createUntrackedSearchParams( | ||
searchParams: ParsedUrlQuery | ||
): ParsedUrlQuery { | ||
const store = staticGenerationAsyncStorage.getStore() | ||
if (store && store.forceStatic) { | ||
return {} | ||
} else { | ||
return searchParams | ||
} | ||
} | ||
|
||
/** | ||
* Takes a ParsedUrlQuery object and returns a Proxy that tracks read access to the object | ||
* | ||
* If running in the browser will always return the provided searchParams object. | ||
* When running during SSR will return empty during a 'force-static' render and | ||
* otherwise it returns a searchParams object which tracks reads to trigger dynamic rendering | ||
* behavior if appropriate | ||
*/ | ||
export function createDynamicallyTrackedSearchParams( | ||
searchParams: ParsedUrlQuery | ||
): ParsedUrlQuery { | ||
const store = staticGenerationAsyncStorage.getStore() | ||
if (!store) { | ||
// we assume we are in a route handler or page render. just return the searchParams | ||
return searchParams | ||
} else if (store.forceStatic) { | ||
// If we forced static we omit searchParams entirely. This is true both during SSR | ||
// and browser render because we need there to be parity between these environments | ||
return {} | ||
} else if (!store.isStaticGeneration && !store.dynamicShouldError) { | ||
// during dynamic renders we don't actually have to track anything so we just return | ||
// the searchParams directly. However if dynamic data access should error then we | ||
// still want to track access. This covers the case in Dev where all renders are dynamic | ||
// but we still want to error if you use a dynamic data source because it will fail the build | ||
// or revalidate if you do. | ||
return searchParams | ||
} else { | ||
// We need to track dynamic access with a Proxy. We implement get, has, and ownKeys because | ||
// these can all be used to exfiltrate information about searchParams. | ||
return new Proxy({} as ParsedUrlQuery, { | ||
get(target, prop, receiver) { | ||
if (typeof prop === 'string') { | ||
trackDynamicDataAccessed(store, `searchParams.${prop}`) | ||
} | ||
return ReflectAdapter.get(target, prop, receiver) | ||
}, | ||
has(target, prop) { | ||
if (typeof prop === 'string') { | ||
trackDynamicDataAccessed(store, `searchParams.${prop}`) | ||
} | ||
return Reflect.has(target, prop) | ||
}, | ||
ownKeys(target) { | ||
trackDynamicDataAccessed(store, 'searchParams') | ||
return Reflect.ownKeys(target) | ||
}, | ||
}) | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,10 @@ import type { Revalidate } from '../../server/lib/revalidate' | |
|
||
import { createAsyncLocalStorage } from './async-local-storage' | ||
|
||
type PrerenderState = { | ||
hasDynamic: boolean | ||
} | ||
|
||
export interface StaticGenerationStore { | ||
readonly isStaticGeneration: boolean | ||
readonly pagePath?: string | ||
|
@@ -16,12 +20,8 @@ export interface StaticGenerationStore { | |
readonly isRevalidate?: boolean | ||
readonly isUnstableCacheCallback?: boolean | ||
|
||
/** | ||
* If defined, this function when called will throw an error postponing | ||
* rendering during the React render phase. This should not be invoked outside | ||
* of the React render phase as it'll throw an error. | ||
*/ | ||
readonly postpone: ((reason: string) => never) | undefined | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generally we want to avoid these methods of passing code around. Even our ComponentMod technique for exposing RSC scoped functions is really not ideal. In the rewrite to dynamic APIs references to React's postpone API are referenced directly from the modules that need them (transitively through dynamic-rendering.ts). Crucially this allows the postpone to be the "right" one for RSC vs SSR whereas in the previous implementaiton we were using the RSC postpone to postpone in SSR in some cases (this happens to work by accident of the implementation but by no means guaranteed) |
||
// When this exists (is not null) it means we are in a Prerender | ||
prerenderState: null | PrerenderState | ||
gnoff marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
forceDynamic?: boolean | ||
fetchCache?: | ||
|
@@ -36,7 +36,6 @@ export interface StaticGenerationStore { | |
forceStatic?: boolean | ||
dynamicShouldError?: boolean | ||
pendingRevalidates?: Record<string, Promise<any>> | ||
postponeWasTriggered?: boolean | ||
|
||
dynamicUsageDescription?: string | ||
dynamicUsageStack?: string | ||
|
@@ -59,3 +58,13 @@ export type StaticGenerationAsyncStorage = | |
|
||
export const staticGenerationAsyncStorage: StaticGenerationAsyncStorage = | ||
createAsyncLocalStorage() | ||
|
||
export function getExpectedStaticGenerationStore(callingExpression: string) { | ||
const store = staticGenerationAsyncStorage.getStore() | ||
if (!store) { | ||
throw new Error( | ||
`Invariant: \`${callingExpression}\` expects to have staticGenerationAsyncStorage, none available.` | ||
) | ||
} | ||
return store | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason we use
ReflectAdapter
here butReflect
inhas
andownKeys
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm it's a style choice. ReflectAdapter requires me to understand what ReflectAdapter is and why it is being used etc.... If I added ReflectAdapter for all the Reflect methods it adds some cognative overhead to later understanding of this code. The fact that only get has special handling in the Adapter to me is a sign that we should probably make a lint rule for binding function types in get traps and just drop the namespaced class.
If I implemented all of Reflect in ReflectAdapter for completeness sake then we'd be shipping more code and in some cases an extra function call that isn't optimized away for the appearence of abstraction (the ReflectAdapter is going to take care of things). If our goal is to make sure you don't forget something critical I'd prefer linting since there is no runtime overhead and the code will just real plainly showing what it does