Skip to content

Commit

Permalink
Pathname and search params SSG (#41247)
Browse files Browse the repository at this point in the history
Bailout to client rendering when using useSearchParams and usePathName
during SSG.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
Hannes Bornö committed Oct 25, 2022
1 parent 2277eb9 commit 1d596ca
Show file tree
Hide file tree
Showing 16 changed files with 337 additions and 36 deletions.
18 changes: 1 addition & 17 deletions packages/next/client/components/headers.ts
@@ -1,21 +1,5 @@
import { DynamicServerError } from './hooks-server-context'
import { requestAsyncStorage } from './request-async-storage'
import { staticGenerationAsyncStorage } from './static-generation-async-storage'

function staticGenerationBailout(reason: string) {
const staticGenerationStore =
staticGenerationAsyncStorage && 'getStore' in staticGenerationAsyncStorage
? staticGenerationAsyncStorage?.getStore()
: staticGenerationAsyncStorage

if (staticGenerationStore?.isStaticGeneration) {
// TODO: honor the dynamic: 'force-static'
if (staticGenerationStore) {
staticGenerationStore.revalidate = 0
}
throw new DynamicServerError(reason)
}
}
import { staticGenerationBailout } from './static-generation-bailout'

export function headers() {
staticGenerationBailout('headers')
Expand Down
3 changes: 3 additions & 0 deletions packages/next/client/components/navigation.ts
Expand Up @@ -12,6 +12,7 @@ import {
PathnameContext,
// LayoutSegmentsContext,
} from './hooks-client-context'
import { staticGenerationBailout } from './static-generation-bailout'

const INTERNAL_URLSEARCHPARAMS_INSTANCE = Symbol(
'internal for urlsearchparams readonly'
Expand Down Expand Up @@ -69,6 +70,7 @@ class ReadonlyURLSearchParams {
* Learn more about URLSearchParams here: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
*/
export function useSearchParams() {
staticGenerationBailout('useSearchParams')
const searchParams = useContext(SearchParamsContext)
const readonlySearchParams = useMemo(() => {
return new ReadonlyURLSearchParams(searchParams)
Expand All @@ -80,6 +82,7 @@ export function useSearchParams() {
* Get the current pathname. For example usePathname() on /dashboard?foo=bar would return "/dashboard"
*/
export function usePathname(): string {
staticGenerationBailout('usePathname')
return useContext(PathnameContext)
}

Expand Down
17 changes: 17 additions & 0 deletions packages/next/client/components/static-generation-bailout.ts
@@ -0,0 +1,17 @@
import { DynamicServerError } from './hooks-server-context'
import { staticGenerationAsyncStorage } from './static-generation-async-storage'

export function staticGenerationBailout(reason: string) {
const staticGenerationStore =
staticGenerationAsyncStorage && 'getStore' in staticGenerationAsyncStorage
? staticGenerationAsyncStorage?.getStore()
: staticGenerationAsyncStorage

if (staticGenerationStore?.isStaticGeneration) {
// TODO: honor the dynamic: 'force-static'
if (staticGenerationStore) {
staticGenerationStore.revalidate = 0
}
throw new DynamicServerError(reason)
}
}
82 changes: 78 additions & 4 deletions test/e2e/app-dir/app-static.test.ts
Expand Up @@ -10,6 +10,8 @@ import webdriver from 'next-webdriver'
const glob = promisify(globOrig)

describe('app-dir static/dynamic handling', () => {
const isDev = (global as any).isNextDev

if ((global as any).isNextDeploy) {
it('should skip next deploy for now', () => {})
return
Expand Down Expand Up @@ -56,6 +58,8 @@ describe('app-dir static/dynamic handling', () => {
'blog/tim/first-post.rsc',
'dynamic-no-gen-params-ssr/[slug]/page.js',
'dynamic-no-gen-params/[slug]/page.js',
'hooks/use-pathname/[slug]/page.js',
'hooks/use-search-params/[slug]/page.js',
'ssr-auto/cache-no-store/page.js',
'ssr-auto/fetch-revalidate-zero/page.js',
'ssr-forced/page.js',
Expand Down Expand Up @@ -359,9 +363,79 @@ describe('app-dir static/dynamic handling', () => {
expect(secondDate).not.toBe(initialDate)
})

it('should show a message to leave feedback for `appDir`', async () => {
expect(next.cliOutput).toContain(
`Thank you for testing \`appDir\` please leave your feedback at https://nextjs.link/app-feedback`
)
describe('hooks', () => {
describe('useSearchParams', () => {
if (isDev) {
it('should bail out to client rendering during SSG', async () => {
const res = await fetchViaHTTP(
next.url,
'/hooks/use-search-params/slug'
)
const html = await res.text()
expect(html).toInclude('<html id="__next_error__">')
})
}

it('should have the correct values', async () => {
const browser = await webdriver(
next.url,
'/hooks/use-search-params/slug?first=value&second=other&third'
)

expect(await browser.elementByCss('#params-first').text()).toBe('value')
expect(await browser.elementByCss('#params-second').text()).toBe(
'other'
)
expect(await browser.elementByCss('#params-third').text()).toBe('')
expect(await browser.elementByCss('#params-not-real').text()).toBe(
'N/A'
)
})

it('should have values from canonical url on rewrite', async () => {
const browser = await webdriver(
next.url,
'/rewritten-use-search-params?first=a&second=b&third=c'
)

expect(await browser.elementByCss('#params-first').text()).toBe('a')
expect(await browser.elementByCss('#params-second').text()).toBe('b')
expect(await browser.elementByCss('#params-third').text()).toBe('c')
expect(await browser.elementByCss('#params-not-real').text()).toBe(
'N/A'
)
})
})

describe('usePathname', () => {
if (isDev) {
it('should bail out to client rendering during SSG', async () => {
const res = await fetchViaHTTP(next.url, '/hooks/use-pathname/slug')
const html = await res.text()
expect(html).toInclude('<html id="__next_error__">')
})
}

it('should have the correct values', async () => {
const browser = await webdriver(next.url, '/hooks/use-pathname/slug')

expect(await browser.elementByCss('#pathname').text()).toBe(
'/hooks/use-pathname/slug'
)
})

it('should have values from canonical url on rewrite', async () => {
const browser = await webdriver(next.url, '/rewritten-use-pathname')

expect(await browser.elementByCss('#pathname').text()).toBe(
'/rewritten-use-pathname'
)
})
})
it('should show a message to leave feedback for `appDir`', async () => {
expect(next.cliOutput).toContain(
`Thank you for testing \`appDir\` please leave your feedback at https://nextjs.link/app-feedback`
)
})
})
})
@@ -0,0 +1,7 @@
export default function Layout({ children }) {
return children
}

export function generateStaticParams() {
return [{ slug: 'slug' }]
}
12 changes: 12 additions & 0 deletions test/e2e/app-dir/app-static/app/hooks/use-pathname/[slug]/page.js
@@ -0,0 +1,12 @@
'use client'
import { usePathname } from 'next/navigation'

export const config = {
dynamicParams: false,
}

export default function Page() {
const pathname = usePathname()

return <p id="pathname">{pathname}</p>
}
@@ -0,0 +1,7 @@
export default function Layout({ children }) {
return children
}

export function generateStaticParams() {
return [{ slug: 'slug' }]
}
@@ -0,0 +1,19 @@
'use client'
import { useSearchParams } from 'next/navigation'

export const config = {
dynamicParams: false,
}

export default function Page() {
const params = useSearchParams()

return (
<>
<p id="params-first">{params.get('first') ?? 'N/A'}</p>
<p id="params-second">{params.get('second') ?? 'N/A'}</p>
<p id="params-third">{params.get('third') ?? 'N/A'}</p>
<p id="params-not-real">{params.get('notReal') ?? 'N/A'}</p>
</>
)
}
9 changes: 9 additions & 0 deletions test/e2e/app-dir/app-static/next.config.js
Expand Up @@ -11,6 +11,15 @@ module.exports = {
source: '/rewritten-to-dashboard',
destination: '/dashboard',
},
{
source: '/rewritten-use-search-params',
destination:
'/hooks/use-search-params/slug?first=value&second=other%20value&third',
},
{
source: '/rewritten-use-pathname',
destination: '/hooks/use-pathname/slug',
},
],
}
},
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/app-dir/app/app/hooks/use-pathname/page.js
Expand Up @@ -8,7 +8,7 @@ export default function Page() {
return (
<>
<h1 id="pathname" data-pathname={pathname}>
hello from /hooks/use-pathname
hello from {pathname}
</h1>
</>
)
Expand Down
13 changes: 4 additions & 9 deletions test/e2e/app-dir/app/app/hooks/use-search-params/page.js
Expand Up @@ -7,15 +7,10 @@ export default function Page() {

return (
<>
<h1
id="params"
data-param-first={params.get('first') ?? 'N/A'}
data-param-second={params.get('second') ?? 'N/A'}
data-param-third={params.get('third') ?? 'N/A'}
data-param-not-real={params.get('notReal') ?? 'N/A'}
>
hello from /hooks/use-search-params
</h1>
<p id="params-first">{params.get('first') ?? 'N/A'}</p>
<p id="params-second">{params.get('second') ?? 'N/A'}</p>
<p id="params-third">{params.get('third') ?? 'N/A'}</p>
<p id="params-not-real">{params.get('notReal') ?? 'N/A'}</p>
</>
)
}
15 changes: 15 additions & 0 deletions test/e2e/app-dir/app/app/search-params-prop/page.js
@@ -0,0 +1,15 @@
'use client'

export default function Page({ searchParams }) {
return (
<h1
id="params"
data-param-first={searchParams.first ?? 'N/A'}
data-param-second={searchParams.second ?? 'N/A'}
data-param-third={searchParams.third ?? 'N/A'}
data-param-not-real={searchParams.notReal ?? 'N/A'}
>
hello from searchParams prop client
</h1>
)
}
13 changes: 13 additions & 0 deletions test/e2e/app-dir/app/app/search-params-prop/server/page.js
@@ -0,0 +1,13 @@
export default function Page({ searchParams }) {
return (
<h1
id="params"
data-param-first={searchParams.first ?? 'N/A'}
data-param-second={searchParams.second ?? 'N/A'}
data-param-third={searchParams.third ?? 'N/A'}
data-param-not-real={searchParams.notReal ?? 'N/A'}
>
hello from searchParams prop server
</h1>
)
}
20 changes: 20 additions & 0 deletions test/e2e/app-dir/app/middleware.js
Expand Up @@ -42,4 +42,24 @@ export function middleware(request) {

return NextResponse[method](new URL('/internal/success', request.url))
}

if (request.nextUrl.pathname === '/search-params-prop-middleware-rewrite') {
return NextResponse.rewrite(
new URL(
'/search-params-prop?first=value&second=other%20value&third',
request.url
)
)
}

if (
request.nextUrl.pathname === '/search-params-prop-server-middleware-rewrite'
) {
return NextResponse.rewrite(
new URL(
'/search-params-prop/server?first=value&second=other%20value&third',
request.url
)
)
}
}
19 changes: 19 additions & 0 deletions test/e2e/app-dir/app/next.config.js
Expand Up @@ -12,6 +12,25 @@ module.exports = {
source: '/rewritten-to-dashboard',
destination: '/dashboard',
},
{
source: '/search-params-prop-rewrite',
destination:
'/search-params-prop?first=value&second=other%20value&third',
},
{
source: '/search-params-prop-server-rewrite',
destination:
'/search-params-prop/server?first=value&second=other%20value&third',
},
{
source: '/rewritten-use-search-params',
destination:
'/hooks/use-search-params?first=value&second=other%20value&third',
},
{
source: '/rewritten-use-pathname',
destination: '/hooks/use-pathname',
},
{
source: '/hooks/use-selected-layout-segment/rewritten',
destination:
Expand Down

0 comments on commit 1d596ca

Please sign in to comment.