Skip to content

Commit 2e669b3

Browse files
committed
Track search string as part of "refresh state"
When a parallel route does not match the current URL, in its place we render the render the last active route that matched that slot. When we next refresh the page, we track some additional state to inform the router how to refresh the old, "inactive" routes that aren't reachable from the outermost, "active" route. Before this PR, the only state we tracked was the URL of the page that last matched the inactive segment. After this PR, I am also tracking the search query string of the old route. This can't be inferred from the URL alone because the URL may have been rewritten by the server. We don't need the search query to perform a network request — that is handled by the URL alone — but we do need it in order to construct a cache key for the page segments. Today we read the cache key from the FlightRouterState, which has all the params embdeed inside of it, but eventually I want to remove all uses of FlightRouterState that aren't about sending/receiving data from the server or storing the tree as state in the browser's history entry. So this is an incremental step towards that goal.
1 parent 4112472 commit 2e669b3

File tree

10 files changed

+85
-70
lines changed

10 files changed

+85
-70
lines changed

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

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
CacheNodeSeedData,
33
FlightRouterState,
44
FlightSegmentPath,
5+
RefreshState,
56
} from '../../../shared/lib/app-router-types'
67
import type {
78
ChildSegmentMap,
@@ -51,7 +52,7 @@ export type NavigationTask = {
5152
// The URL that should be used to fetch the dynamic data. This is only set
5253
// when the segment cannot be refetched from the current route, because it's
5354
// part of a "default" parallel slot that was reused during a navigation.
54-
refreshUrl: string | null
55+
refreshState: RefreshState | null
5556
children: Map<string, NavigationTask> | null
5657
}
5758

@@ -175,6 +176,7 @@ export function createInitialCacheNodeForHydration(
175176
export function startPPRNavigation(
176177
navigatedAt: number,
177178
oldUrl: URL,
179+
oldRenderedSearch: string,
178180
oldCacheNode: CacheNode | null,
179181
oldRouterState: FlightRouterState,
180182
newRouterState: FlightRouterState,
@@ -189,7 +191,11 @@ export function startPPRNavigation(
189191
): NavigationTask | null {
190192
const didFindRootLayout = false
191193
const parentNeedsDynamicRequest = false
192-
const parentRefreshUrl = null
194+
const parentRefreshState = null
195+
const oldRootRefreshState: RefreshState = [
196+
createHrefFromUrl(oldUrl),
197+
oldRenderedSearch,
198+
]
193199
return updateCacheNodeOnNavigation(
194200
navigatedAt,
195201
oldUrl,
@@ -207,7 +213,8 @@ export function startPPRNavigation(
207213
null,
208214
null,
209215
parentNeedsDynamicRequest,
210-
parentRefreshUrl,
216+
oldRootRefreshState,
217+
parentRefreshState,
211218
accumulation
212219
)
213220
}
@@ -229,7 +236,8 @@ function updateCacheNodeOnNavigation(
229236
parentSegmentPath: FlightSegmentPath | null,
230237
parentParallelRouteKey: string | null,
231238
parentNeedsDynamicRequest: boolean,
232-
parentRefreshUrl: string | null,
239+
oldRootRefreshState: RefreshState,
240+
parentRefreshState: RefreshState | null,
233241
accumulation: NavigationRequestAccumulation
234242
): NavigationTask | null {
235243
// Check if this segment matches the one in the previous route.
@@ -441,20 +449,20 @@ function updateCacheNodeOnNavigation(
441449
// current route; it may have been reused from an older route. If so,
442450
// we need to fetch its data from the old route's URL rather than current
443451
// route's URL. Keep track of this as we traverse the tree.
444-
const href = newRouterState[2]
445-
const refreshUrl =
446-
typeof href === 'string' && newRouterState[3] === 'refresh'
452+
const maybeRefreshState = newRouterState[2]
453+
const refreshState =
454+
maybeRefreshState !== undefined && maybeRefreshState !== null
447455
? // This segment is not present in the current route. Track its
448456
// refresh URL as we continue traversing the tree.
449-
href
457+
maybeRefreshState
450458
: // Inherit the refresh URL from the parent.
451-
parentRefreshUrl
459+
parentRefreshState
452460

453461
// If this segment itself needs to fetch new data from the server, then by
454462
// definition it is being refreshed. Track its refresh URL so we know which
455463
// URL to request the data from.
456-
if (needsDynamicRequest && refreshUrl !== null) {
457-
accumulateRefreshUrl(accumulation, refreshUrl)
464+
if (needsDynamicRequest && refreshState !== null) {
465+
accumulateRefreshUrl(accumulation, refreshState)
458466
}
459467

460468
// As we diff the trees, we may sometimes modify (copy-on-write, not mutate)
@@ -519,7 +527,7 @@ function updateCacheNodeOnNavigation(
519527
// a soft navigation; instead, the client reuses whatever segment was
520528
// already active in that slot on the previous route.
521529
newRouterStateChild = reuseActiveSegmentInDefaultSlot(
522-
oldUrl,
530+
oldRootRefreshState,
523531
oldRouterStateChild
524532
)
525533
newSegmentChild = newRouterStateChild[0]
@@ -556,7 +564,8 @@ function updateCacheNodeOnNavigation(
556564
segmentPath,
557565
parallelRouteKey,
558566
parentNeedsDynamicRequest || needsDynamicRequest,
559-
refreshUrl,
567+
oldRootRefreshState,
568+
refreshState,
560569
accumulation
561570
)
562571

@@ -613,7 +622,7 @@ function updateCacheNodeOnNavigation(
613622
childNeedsDynamicRequest,
614623
parentNeedsDynamicRequest
615624
),
616-
refreshUrl,
625+
refreshState,
617626
children: taskChildren,
618627
}
619628
}
@@ -918,7 +927,7 @@ function createCacheNodeOnNavigation(
918927
),
919928
// This route is not part of the current tree, so there's no reason to
920929
// track the refresh URL.
921-
refreshUrl: null,
930+
refreshState: null,
922931
children: taskChildren,
923932
}
924933
}
@@ -981,7 +990,7 @@ function createDynamicRequestTree(
981990

982991
function accumulateRefreshUrl(
983992
accumulation: NavigationRequestAccumulation,
984-
refreshUrl: string
993+
refreshState: RefreshState
985994
) {
986995
// This is a refresh navigation, and we're inside a "default" slot that's
987996
// not part of the current route; it was reused from an older route. In
@@ -993,6 +1002,7 @@ function accumulateRefreshUrl(
9931002
// we don't do it immediately here is so we can deduplicate multiple
9941003
// instances of the same URL into a single request. See
9951004
// listenForDynamicRequest for more details.
1005+
const refreshUrl = refreshState[0]
9961006
const separateRefreshUrls = accumulation.separateRefreshUrls
9971007
if (separateRefreshUrls === null) {
9981008
accumulation.separateRefreshUrls = new Set([refreshUrl])
@@ -1002,34 +1012,30 @@ function accumulateRefreshUrl(
10021012
}
10031013

10041014
function reuseActiveSegmentInDefaultSlot(
1005-
oldUrl: URL,
1015+
oldRootRefreshState: RefreshState,
10061016
oldRouterState: FlightRouterState
10071017
): FlightRouterState {
10081018
// This is a "default" segment. These are never sent by the server during a
10091019
// soft navigation; instead, the client reuses whatever segment was already
10101020
// active in that slot on the previous route. This means if we later need to
10111021
// refresh the segment, it will have to be refetched from the previous route's
10121022
// URL. We store it in the Flight Router State.
1013-
//
1014-
// TODO: We also mark the segment with a "refresh" marker but I think we can
1015-
// get rid of that eventually by making sure we only add URLs to page segments
1016-
// that are reused. Then the presence of the URL alone is enough.
10171023
let reusedRouterState
10181024

1019-
const oldRefreshMarker = oldRouterState[3]
1020-
if (oldRefreshMarker === 'refresh') {
1025+
const oldRefreshState = oldRouterState[2]
1026+
if (oldRefreshState !== undefined && oldRefreshState !== null) {
10211027
// This segment was already reused from an even older route. Keep its
1022-
// existing URL and refresh marker.
1028+
// existing URL and refresh state.
10231029
reusedRouterState = oldRouterState
10241030
} else {
1025-
// This segment was not previously reused, and it's not on the new route.
1026-
// So it must have been delivered in the old route.
1031+
// Since this route didn't already have a refresh state, it must have been
1032+
// reachable from the root of the old route. So we use the refresh state
1033+
// that represents the old route.
10271034
reusedRouterState = patchRouterStateWithNewChildren(
10281035
oldRouterState,
10291036
oldRouterState[1]
10301037
)
1031-
reusedRouterState[2] = createHrefFromUrl(oldUrl)
1032-
reusedRouterState[3] = 'refresh'
1038+
reusedRouterState[2] = oldRootRefreshState
10331039
}
10341040

10351041
return reusedRouterState
@@ -1626,7 +1632,7 @@ function abortRemainingPendingTasks(
16261632
//
16271633
// When this happens, we treat this the same as a refresh(). The entire
16281634
// tree will be re-rendered from the root.
1629-
if (task.refreshUrl === null) {
1635+
if (task.refreshState === null) {
16301636
// Trigger a "soft" refresh. Essentially the same as calling `refresh()`
16311637
// in a Server Action.
16321638
exitStatus = NavigationTaskExitStatus.SoftRetry

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,11 @@ export function navigateReducer(
162162
// implementation. Eventually we'll rewrite the router reducer to a
163163
// state machine.
164164
const currentUrl = new URL(state.canonicalUrl, location.origin)
165+
const currentRenderedSearch = state.renderedSearch
165166
const result = navigateUsingSegmentCache(
166167
url,
167168
currentUrl,
169+
currentRenderedSearch,
168170
state.cache,
169171
state.tree,
170172
state.nextUrl,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function refreshDynamicData(
3636
// existing dynamic data (including in shared layouts) is re-fetched.
3737
const currentCanonicalUrl = state.canonicalUrl
3838
const currentUrl = new URL(currentCanonicalUrl, location.origin)
39+
const currentRenderedSearch = state.renderedSearch
3940
const currentFlightRouterState = state.tree
4041
const shouldScroll = true
4142

@@ -53,6 +54,7 @@ export function refreshDynamicData(
5354
currentCanonicalUrl,
5455
navigationSeed,
5556
currentUrl,
57+
currentRenderedSearch,
5658
state.cache,
5759
currentFlightRouterState,
5860
freshnessPolicy,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export function restoreReducer(
5050
const task = startPPRNavigation(
5151
now,
5252
currentUrl,
53+
state.renderedSearch,
5354
state.cache,
5455
state.tree,
5556
treeToRestore,

packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ export function serverActionReducer(
382382
// If there was no redirect, then the target URL is the same as the
383383
// current URL.
384384
const currentUrl = new URL(state.canonicalUrl, location.origin)
385+
const currentRenderedSearch = state.renderedSearch
385386
const redirectUrl =
386387
redirectLocation !== undefined ? redirectLocation : currentUrl
387388
const currentFlightRouterState = state.tree
@@ -425,6 +426,7 @@ export function serverActionReducer(
425426
redirectCanonicalUrl,
426427
navigationSeed,
427428
currentUrl,
429+
currentRenderedSearch,
428430
state.cache,
429431
currentFlightRouterState,
430432
freshnessPolicy,
@@ -446,6 +448,7 @@ export function serverActionReducer(
446448
const result = navigateUsingSegmentCache(
447449
redirectUrl,
448450
currentUrl,
451+
currentRenderedSearch,
449452
state.cache,
450453
currentFlightRouterState,
451454
nextUrl,

packages/next/src/client/components/router-reducer/reducers/server-patch-reducer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function serverPatchReducer(
3030
return handleExternalUrl(state, mutable, retryUrl.href, false)
3131
}
3232
const currentUrl = new URL(state.canonicalUrl, location.origin)
33+
const currentRenderedSearch = state.renderedSearch
3334
if (action.previousTree !== state.tree) {
3435
// There was another, more recent navigation since the once that
3536
// mismatched. We can abort the retry, but we still need to refresh the
@@ -50,6 +51,7 @@ export function serverPatchReducer(
5051
retryCanonicalUrl,
5152
retrySeed,
5253
currentUrl,
54+
currentRenderedSearch,
5355
state.cache,
5456
state.tree,
5557
FreshnessPolicy.RefreshAll,

packages/next/src/client/components/router-reducer/router-reducer-types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,17 @@ export type AppRouterState = {
222222
* - This is the url you see in the browser.
223223
*/
224224
canonicalUrl: string
225+
226+
/**
227+
* The search query observed by the server during rendering. This may be
228+
* different from the canonical URL's search query if the server performed
229+
* a rewrite. Even though a client component won't observe this (unless it
230+
* were passed from a Server component), the client router needs to know this
231+
* so it can properly cache segment data; it'ss part of a page segment's
232+
* cache key.
233+
*/
225234
renderedSearch: string
235+
226236
/**
227237
* The underlying "url" representing the UI state, which is used for intercepting routes.
228238
*/

packages/next/src/client/components/segment-cache/navigation.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export type NavigationResult =
7070
export function navigate(
7171
url: URL,
7272
currentUrl: URL,
73+
currentRenderedSearch: string,
7374
currentCacheNode: CacheNode | null,
7475
currentFlightRouterState: FlightRouterState,
7576
nextUrl: string | null,
@@ -122,6 +123,7 @@ export function navigate(
122123
now,
123124
url,
124125
currentUrl,
126+
currentRenderedSearch,
125127
nextUrl,
126128
isSamePageNavigation,
127129
currentCacheNode,
@@ -166,6 +168,7 @@ export function navigate(
166168
now,
167169
url,
168170
currentUrl,
171+
currentRenderedSearch,
169172
nextUrl,
170173
isSamePageNavigation,
171174
currentCacheNode,
@@ -193,6 +196,7 @@ export function navigate(
193196
now,
194197
url,
195198
currentUrl,
199+
currentRenderedSearch,
196200
nextUrl,
197201
currentCacheNode,
198202
currentFlightRouterState,
@@ -209,6 +213,7 @@ export function navigateToSeededRoute(
209213
canonicalUrl: string,
210214
navigationSeed: NavigationSeed,
211215
currentUrl: URL,
216+
currentRenderedSearch: string,
212217
currentCacheNode: CacheNode | null,
213218
currentFlightRouterState: FlightRouterState,
214219
freshnessPolicy: FreshnessPolicy,
@@ -225,6 +230,7 @@ export function navigateToSeededRoute(
225230
const task = startPPRNavigation(
226231
now,
227232
currentUrl,
233+
currentRenderedSearch,
228234
currentCacheNode,
229235
currentFlightRouterState,
230236
navigationSeed.tree,
@@ -259,6 +265,7 @@ function navigateUsingPrefetchedRouteTree(
259265
now: number,
260266
url: URL,
261267
currentUrl: URL,
268+
currentRenderedSearch: string,
262269
nextUrl: string | null,
263270
isSamePageNavigation: boolean,
264271
currentCacheNode: CacheNode | null,
@@ -287,6 +294,7 @@ function navigateUsingPrefetchedRouteTree(
287294
const task = startPPRNavigation(
288295
now,
289296
currentUrl,
297+
currentRenderedSearch,
290298
currentCacheNode,
291299
currentFlightRouterState,
292300
prefetchFlightRouterState,
@@ -483,6 +491,7 @@ async function navigateDynamicallyWithNoPrefetch(
483491
now: number,
484492
url: URL,
485493
currentUrl: URL,
494+
currentRenderedSearch: string,
486495
nextUrl: string | null,
487496
currentCacheNode: CacheNode | null,
488497
currentFlightRouterState: FlightRouterState,
@@ -558,6 +567,7 @@ async function navigateDynamicallyWithNoPrefetch(
558567
createHrefFromUrl(canonicalUrl),
559568
navigationSeed,
560569
currentUrl,
570+
currentRenderedSearch,
561571
currentCacheNode,
562572
currentFlightRouterState,
563573
freshnessPolicy,

0 commit comments

Comments
 (0)