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

Opt-in to dynamic rendering when reading searchParams #46205

Merged
Merged
2 changes: 2 additions & 0 deletions packages/next/src/build/webpack/loaders/next-app-loader.ts
Expand Up @@ -413,6 +413,8 @@ const nextAppLoader: AppLoader = async function nextAppLoader() {

export { requestAsyncStorage } from 'next/dist/client/components/request-async-storage'

export { staticGenerationBailout } from 'next/dist/client/components/static-generation-bailout'

export * as serverHooks from 'next/dist/client/components/hooks-server-context'

export { renderToReadableStream } from 'next/dist/compiled/react-server-dom-webpack/server.edge'
Expand Down
23 changes: 21 additions & 2 deletions packages/next/src/server/app-render.tsx
Expand Up @@ -815,6 +815,8 @@ export async function renderToHTMLOrFlight(
ComponentMod.staticGenerationAsyncStorage
const requestAsyncStorage: RequestAsyncStorage =
ComponentMod.requestAsyncStorage
const staticGenerationBailout =
ComponentMod.staticGenerationBailout as typeof import('../client/components/static-generation-bailout').staticGenerationBailout

// we wrap the render in an AsyncLocalStorage context
const wrappedRender = async () => {
Expand Down Expand Up @@ -860,10 +862,27 @@ export async function renderToHTMLOrFlight(
? crypto.randomUUID()
: require('next/dist/compiled/nanoid').nanoid()

const searchParamsProps = { searchParams: query }

stripInternalQueries(query)

const providedSearchParams = new Proxy(query, {
get(target, prop) {
const targetValue = (target as any)[prop]
// React adds some properties on the object when serializing for client components
if (
typeof prop === 'string' &&
prop !== 'toJSON' &&
prop !== 'toString' &&
prop !== '$$typeof' &&
prop !== 'then'
) {
staticGenerationBailout(`searchParams.${prop}`)
}
return targetValue
},
})

const searchParamsProps = { searchParams: providedSearchParams }

const LayoutRouter =
ComponentMod.LayoutRouter as typeof import('../client/components/layout-router').default
const RenderFromTemplateContext =
Expand Down
2 changes: 1 addition & 1 deletion plopfile.js
Expand Up @@ -9,7 +9,7 @@ module.exports = function (plop) {
type: 'confirm',
name: 'appDir',
message: 'Is this test for the app directory?',
default: false,
default: true,
},
{
type: 'input',
Expand Down
@@ -0,0 +1,8 @@
'use client'
export default function ClientComponent({ searchParams }) {
return (
<>
<h1>Parameter: {searchParams.search}</h1>
</>
)
}
@@ -0,0 +1,11 @@
import { nanoid } from 'nanoid'
import ClientComponent from './component'

export default function Page({ searchParams }) {
return (
<>
<ClientComponent searchParams={searchParams} />
<p id="nanoid">{nanoid()}</p>
</>
)
}
7 changes: 7 additions & 0 deletions test/e2e/app-dir/searchparams-static-bailout/app/layout.tsx
@@ -0,0 +1,7 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/searchparams-static-bailout/app/page.tsx
@@ -0,0 +1,10 @@
import { nanoid } from 'nanoid'

export default function Page({ searchParams }) {
return (
<>
<h1>Parameter: {searchParams.search}</h1>
<p id="nanoid">{nanoid()}</p>
</>
)
}
8 changes: 8 additions & 0 deletions test/e2e/app-dir/searchparams-static-bailout/next.config.js
@@ -0,0 +1,8 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: { appDir: true },
}

module.exports = nextConfig
@@ -0,0 +1,38 @@
import { createNextDescribe } from 'e2e-utils'

createNextDescribe(
'searchparams-static-bailout',
{
files: __dirname,
dependencies: {
nanoid: '4.0.1',
},
},
({ next, isNextStart }) => {
// Recommended for tests that check HTML. Cheerio is a HTML parser that has a jQuery like API.
it('should work be part of the initial html', async () => {
const $ = await next.render$('/?search=hello')
expect($('h1').text()).toBe('Parameter: hello')

// Check if the page is not statically generated.
if (isNextStart) {
const id = $('#nanoid').text()
const $2 = await next.render$('/?search=hello')
const id2 = $2('#nanoid').text()
expect(id).not.toBe(id2)
}
})

it('should work using browser', async () => {
const browser = await next.browser('/?search=hello')
expect(await browser.elementByCss('h1').text()).toBe('Parameter: hello')
// Check if the page is not statically generated.
if (isNextStart) {
const id = await browser.elementByCss('#nanoid').text()
const browser2 = await next.browser('/?search=hello')
const id2 = browser2.elementByCss('#nanoid').text()
expect(id).not.toBe(id2)
}
})
}
)
24 changes: 24 additions & 0 deletions test/e2e/app-dir/searchparams-static-bailout/tsconfig.json
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}