Skip to content

Commit 5ccc907

Browse files
authored
Add rendered search to router state (#84983)
The "rendered search" represents the search params that are observed on the server, i.e. the ones passed as the `searchParams` prop to page segments. They may differ from the search params in the browser's URL bar, if the server performed a rewrite. This case corresponds to the x-nextjs-rewritten-query header. If a page segment is marked with `"use client"`, we can use this value to compute the search params on the client instead of having to fetch them from the server. This part is implemented in #84883.
1 parent 1bb10fc commit 5ccc907

19 files changed

+169
-108
lines changed

packages/next/src/client/app-index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ export function hydrate(
284284
navigatedAt: initialTimestamp,
285285
initialFlightData: initialRSCPayload.f,
286286
initialCanonicalUrlParts: initialRSCPayload.c,
287+
initialRenderedSearch: initialRSCPayload.q,
287288
initialParallelRoutes: new Map(),
288289
location: window.location,
289290
}),

packages/next/src/client/components/app-router-instance.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type NavigateAction,
1010
ACTION_HMR_REFRESH,
1111
PrefetchKind,
12+
type AppHistoryState,
1213
} from './router-reducer/router-reducer-types'
1314
import { reducer } from './router-reducer/router-reducer'
1415
import { startTransition } from 'react'
@@ -27,7 +28,6 @@ import type {
2728
PrefetchOptions,
2829
} from '../../shared/lib/app-router-context.shared-runtime'
2930
import { setLinkForCurrentNavigation, type LinkInstance } from './links'
30-
import type { FlightRouterState } from '../../shared/lib/app-router-types'
3131
import type { ClientInstrumentationHooks } from '../app-index'
3232
import type { GlobalErrorComponent } from './builtin/global-error'
3333

@@ -305,7 +305,7 @@ export function dispatchNavigateAction(
305305

306306
export function dispatchTraverseAction(
307307
href: string,
308-
tree: FlightRouterState | undefined
308+
historyState: AppHistoryState | undefined
309309
) {
310310
const onRouterTransitionStart = getProfilingHookForOnNavigationStart()
311311
if (onRouterTransitionStart !== null) {
@@ -314,7 +314,7 @@ export function dispatchTraverseAction(
314314
dispatchAppRouterAction({
315315
type: ACTION_RESTORE,
316316
url: new URL(href),
317-
tree,
317+
historyState,
318318
})
319319
}
320320

packages/next/src/client/components/app-router.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import {
1010
LayoutRouterContext,
1111
GlobalLayoutRouterContext,
1212
} from '../../shared/lib/app-router-context.shared-runtime'
13-
import type {
14-
CacheNode,
15-
FlightRouterState,
16-
} from '../../shared/lib/app-router-types'
13+
import type { CacheNode } from '../../shared/lib/app-router-types'
1714
import { ACTION_RESTORE } from './router-reducer/router-reducer-types'
18-
import type { AppRouterState } from './router-reducer/router-reducer-types'
15+
import type {
16+
AppHistoryState,
17+
AppRouterState,
18+
} from './router-reducer/router-reducer-types'
1919
import { createHrefFromUrl } from './router-reducer/create-href-from-url'
2020
import {
2121
SearchParamsContext,
@@ -61,14 +61,20 @@ function HistoryUpdater({
6161
window.next.__pendingUrl = undefined
6262
}
6363

64-
const { tree, pushRef, canonicalUrl } = appRouterState
64+
const { tree, pushRef, canonicalUrl, renderedSearch } = appRouterState
65+
66+
const appHistoryState: AppHistoryState = {
67+
tree,
68+
renderedSearch,
69+
}
70+
6571
const historyState = {
6672
...(pushRef.preserveCustomHistoryState ? window.history.state : {}),
6773
// Identifier is shortened intentionally.
6874
// __NA is used to identify if the history entry can be handled by the app-router.
6975
// __N is used to identify if the history entry can be handled by the old router.
7076
__NA: true,
71-
__PRIVATE_NEXTJS_INTERNALS_TREE: tree,
77+
__PRIVATE_NEXTJS_INTERNALS_TREE: appHistoryState,
7278
}
7379
if (
7480
pushRef.pendingPush &&
@@ -217,7 +223,7 @@ function Router({
217223
dispatchAppRouterAction({
218224
type: ACTION_RESTORE,
219225
url: new URL(window.location.href),
220-
tree: window.history.state.__PRIVATE_NEXTJS_INTERNALS_TREE,
226+
historyState: window.history.state.__PRIVATE_NEXTJS_INTERNALS_TREE,
221227
})
222228
}
223229

@@ -300,14 +306,14 @@ function Router({
300306
url: string | URL | null | undefined
301307
) => {
302308
const href = window.location.href
303-
const tree: FlightRouterState | undefined =
309+
const appHistoryState: AppHistoryState | undefined =
304310
window.history.state?.__PRIVATE_NEXTJS_INTERNALS_TREE
305311

306312
startTransition(() => {
307313
dispatchAppRouterAction({
308314
type: ACTION_RESTORE,
309315
url: new URL(url ?? href, href),
310-
tree,
316+
historyState: appHistoryState,
311317
})
312318
})
313319
}

packages/next/src/client/components/router-reducer/aliased-prefetch-navigations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function handleAliasedPrefetchEntry(
3131
state: ReadonlyReducerState,
3232
flightData: string | NormalizedFlightData[],
3333
url: URL,
34+
renderedSearch: string,
3435
mutable: Mutable
3536
) {
3637
let currentTree = state.tree
@@ -140,6 +141,7 @@ export function handleAliasedPrefetchEntry(
140141
}
141142

142143
mutable.patchedTree = currentTree
144+
mutable.renderedSearch = renderedSearch
143145
mutable.cache = currentCache
144146
mutable.canonicalUrl = href
145147
mutable.hashFragment = url.hash

packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe('createInitialRouterState', () => {
3838
navigatedAt,
3939
initialFlightData: [[initialTree, ['', children, {}, null]]],
4040
initialCanonicalUrlParts: initialCanonicalUrl.split('/'),
41+
initialRenderedSearch: '',
4142
initialParallelRoutes,
4243
location: new URL('/linking', 'https://localhost') as any,
4344
})
@@ -46,6 +47,7 @@ describe('createInitialRouterState', () => {
4647
navigatedAt,
4748
initialFlightData: [[initialTree, ['', children, {}, null]]],
4849
initialCanonicalUrlParts: initialCanonicalUrl.split('/'),
50+
initialRenderedSearch: '',
4951
initialParallelRoutes,
5052
location: new URL('/linking', 'https://localhost') as any,
5153
})
@@ -102,6 +104,7 @@ describe('createInitialRouterState', () => {
102104
const expected: ReturnType<typeof createInitialRouterState> = {
103105
tree: initialTree,
104106
canonicalUrl: initialCanonicalUrl,
107+
renderedSearch: '',
105108
pushRef: {
106109
pendingPush: false,
107110
mpaNavigation: false,

packages/next/src/client/components/router-reducer/create-initial-router-state.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import type {
66
import { createHrefFromUrl } from './create-href-from-url'
77
import { fillLazyItemsTillLeafWithHead } from './fill-lazy-items-till-leaf-with-head'
88
import { extractPathFromFlightRouterState } from './compute-changed-path'
9+
10+
import type { AppRouterState } from './router-reducer-types'
911
import { addRefreshMarkerToActiveParallelSegments } from './refetch-inactive-parallel-segments'
1012
import { getFlightDataPartsFromPath } from '../../flight-data-helpers'
1113

1214
export interface InitialRouterStateParameters {
1315
navigatedAt: number
1416
initialCanonicalUrlParts: string[]
17+
initialRenderedSearch: string
1518
initialParallelRoutes: CacheNode['parallelRoutes']
1619
initialFlightData: FlightDataPath[]
1720
location: Location | null
@@ -21,9 +24,10 @@ export function createInitialRouterState({
2124
navigatedAt,
2225
initialFlightData,
2326
initialCanonicalUrlParts,
27+
initialRenderedSearch,
2428
initialParallelRoutes,
2529
location,
26-
}: InitialRouterStateParameters) {
30+
}: InitialRouterStateParameters): AppRouterState {
2731
// When initialized on the server, the canonical URL is provided as an array of parts.
2832
// This is to ensure that when the RSC payload streamed to the client, crawlers don't interpret it
2933
// as a URL that should be crawled.
@@ -91,6 +95,7 @@ export function createInitialRouterState({
9195
segmentPaths: [],
9296
},
9397
canonicalUrl,
98+
renderedSearch: initialRenderedSearch,
9499
nextUrl:
95100
// the || operator is intentional, the pathname can be an empty string
96101
(extractPathFromFlightRouterState(initialTree) || location?.pathname) ??

packages/next/src/client/components/router-reducer/fetch-server-response.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ import {
3636
} from '../../flight-data-helpers'
3737
import { getAppBuildId } from '../../app-build-id'
3838
import { setCacheBustingSearchParam } from './set-cache-busting-search-param'
39-
import { urlToUrlWithoutFlightMarker } from '../../route-params'
39+
import {
40+
getRenderedSearch,
41+
urlToUrlWithoutFlightMarker,
42+
} from '../../route-params'
43+
import type { NormalizedSearch } from '../segment-cache'
4044

4145
const createFromReadableStream =
4246
createFromReadableStreamBrowser as (typeof import('react-server-dom-webpack/client.browser'))['createFromReadableStream']
@@ -63,16 +67,23 @@ export interface FetchServerResponseOptions {
6367
readonly isHmrRefresh?: boolean
6468
}
6569

66-
export type FetchServerResponseResult = {
67-
flightData: NormalizedFlightData[] | string
68-
canonicalUrl: URL | undefined
70+
type SpaFetchServerResponseResult = {
71+
flightData: NormalizedFlightData[]
72+
canonicalUrl: URL
73+
renderedSearch: NormalizedSearch
6974
couldBeIntercepted: boolean
7075
prerendered: boolean
7176
postponed: boolean
7277
staleTime: number
7378
debugInfo: Array<any> | null
7479
}
7580

81+
type MpaFetchServerResponseResult = string
82+
83+
export type FetchServerResponseResult =
84+
| MpaFetchServerResponseResult
85+
| SpaFetchServerResponseResult
86+
7687
export type RequestHeaders = {
7788
[RSC_HEADER]?: '1'
7889
[NEXT_ROUTER_STATE_TREE_HEADER]?: string
@@ -88,17 +99,7 @@ export type RequestHeaders = {
8899
}
89100

90101
function doMpaNavigation(url: string): FetchServerResponseResult {
91-
return {
92-
flightData: urlToUrlWithoutFlightMarker(
93-
new URL(url, location.origin)
94-
).toString(),
95-
canonicalUrl: undefined,
96-
couldBeIntercepted: false,
97-
prerendered: false,
98-
postponed: false,
99-
staleTime: -1,
100-
debugInfo: null,
101-
}
102+
return urlToUrlWithoutFlightMarker(new URL(url, location.origin)).toString()
102103
}
103104

104105
let abortController = new AbortController()
@@ -197,7 +198,7 @@ export async function fetchServerResponse(
197198
)
198199

199200
const responseUrl = urlToUrlWithoutFlightMarker(new URL(res.url))
200-
const canonicalUrl = res.redirected ? responseUrl : undefined
201+
const canonicalUrl = res.redirected ? responseUrl : url
201202

202203
const contentType = res.headers.get('content-type') || ''
203204
const interception = !!res.headers.get('vary')?.includes(NEXT_URL)
@@ -266,9 +267,15 @@ export async function fetchServerResponse(
266267
return doMpaNavigation(res.url)
267268
}
268269

270+
const normalizedFlightData = normalizeFlightData(flightResponse.f)
271+
if (typeof normalizedFlightData === 'string') {
272+
return doMpaNavigation(normalizedFlightData)
273+
}
274+
269275
return {
270-
flightData: normalizeFlightData(flightResponse.f),
276+
flightData: normalizedFlightData,
271277
canonicalUrl: canonicalUrl,
278+
renderedSearch: getRenderedSearch(res),
272279
couldBeIntercepted: interception,
273280
prerendered: flightResponse.S,
274281
postponed,
@@ -286,15 +293,7 @@ export async function fetchServerResponse(
286293
// If fetch fails handle it like a mpa navigation
287294
// TODO-APP: Add a test for the case where a CORS request fails, e.g. external url redirect coming from the response.
288295
// See https://github.com/vercel/next.js/issues/43605#issuecomment-1451617521 for a reproduction.
289-
return {
290-
flightData: url.toString(),
291-
canonicalUrl: undefined,
292-
couldBeIntercepted: false,
293-
prerendered: false,
294-
postponed: false,
295-
staleTime: -1,
296-
debugInfo: null,
297-
}
296+
return url.toString()
298297
}
299298
}
300299

packages/next/src/client/components/router-reducer/handle-mutable.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,8 @@ export function handleMutable(
3535

3636
return {
3737
// Set href.
38-
canonicalUrl: isNotUndefined(mutable.canonicalUrl)
39-
? mutable.canonicalUrl === state.canonicalUrl
40-
? state.canonicalUrl
41-
: mutable.canonicalUrl
42-
: state.canonicalUrl,
38+
canonicalUrl: mutable.canonicalUrl ?? state.canonicalUrl,
39+
renderedSearch: mutable.renderedSearch ?? state.renderedSearch,
4340
pushRef: {
4441
pendingPush: isNotUndefined(mutable.pendingPush)
4542
? mutable.pendingPush

packages/next/src/client/components/router-reducer/ppr-navigations.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -790,13 +790,14 @@ export function listenForDynamicRequest(
790790
responsePromise: Promise<FetchServerResponseResult>
791791
) {
792792
responsePromise.then(
793-
({ flightData, debugInfo }: FetchServerResponseResult) => {
794-
if (typeof flightData === 'string') {
793+
(result: FetchServerResponseResult) => {
794+
if (typeof result === 'string') {
795795
// Happens when navigating to page in `pages` from `app`. We shouldn't
796796
// get here because should have already handled this during
797797
// the prefetch.
798798
return
799799
}
800+
const { flightData, debugInfo } = result
800801
for (const normalizedFlightData of flightData) {
801802
const {
802803
segmentPath,

packages/next/src/client/components/router-reducer/reducers/hmr-refresh-reducer.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { fetchServerResponse } from '../fetch-server-response'
1+
import {
2+
fetchServerResponse,
3+
type FetchServerResponseResult,
4+
} from '../fetch-server-response'
25
import { createHrefFromUrl } from '../create-href-from-url'
36
import { applyRouterStatePatchToTree } from '../apply-router-state-patch-to-tree'
47
import { isNavigatingToNewRootLayout } from '../is-navigating-to-new-root-layout'
@@ -42,17 +45,19 @@ function hmrRefreshReducerImpl(
4245
})
4346

4447
return cache.lazyData.then(
45-
({ flightData, canonicalUrl: canonicalUrlOverride }) => {
48+
(result: FetchServerResponseResult) => {
4649
// Handle case when navigating to page in `pages` from `app`
47-
if (typeof flightData === 'string') {
50+
if (typeof result === 'string') {
4851
return handleExternalUrl(
4952
state,
5053
mutable,
51-
flightData,
54+
result,
5255
state.pushRef.pendingPush
5356
)
5457
}
5558

59+
const { flightData, canonicalUrl, renderedSearch } = result
60+
5661
// Remove cache.lazyData as it has been resolved at this point.
5762
cache.lazyData = null
5863

@@ -88,13 +93,6 @@ function hmrRefreshReducerImpl(
8893
)
8994
}
9095

91-
const canonicalUrlOverrideHref = canonicalUrlOverride
92-
? createHrefFromUrl(canonicalUrlOverride)
93-
: undefined
94-
95-
if (canonicalUrlOverride) {
96-
mutable.canonicalUrl = canonicalUrlOverrideHref
97-
}
9896
const applied = applyFlightData(
9997
navigatedAt,
10098
currentCache,
@@ -108,7 +106,8 @@ function hmrRefreshReducerImpl(
108106
}
109107

110108
mutable.patchedTree = newTree
111-
mutable.canonicalUrl = href
109+
mutable.renderedSearch = renderedSearch
110+
mutable.canonicalUrl = createHrefFromUrl(canonicalUrl)
112111

113112
currentTree = newTree
114113
}

0 commit comments

Comments
 (0)