Repository: vercel/next.js
Pull request: [Segment Cache] Add refresh URL to reused default segments

When segment is reused in the "default" parallel route slot, it is not part of the new page. So if the page is refreshed, we must fetch its data from the previous URL, not the new URL. To track this we add the previous URL to a special field in the FlightRouterState.

Currently this field is added by addRefreshMarkerToActiveParallelSegments; this updates the "ppr-navigations" module to add it during the main traversal used by navigations, so we can eventually get rid addRefreshMarkerToActiveParallelSegments.

Note that we don't need to add the marker to _all_ page segments, just the ones that are not part of the new page.

Changed files:
filename: packages/next/src/client/components/router-reducer/ppr-navigations.ts
patch: @@ -15,6 +15,7 @@ import type {
 } from '../../../shared/lib/app-router-types'
 import { DEFAULT_SEGMENT_KEY } from '../../../shared/lib/segment'
 import { matchSegment } from '../match-segments'
+import { createHrefFromUrl } from './create-href-from-url'
 import { createRouterCacheKey } from './create-router-cache-key'
 import type { FetchServerResponseResult } from './fetch-server-response'
 import { isNavigatingToNewRootLayout } from './is-navigating-to-new-root-layout'
@@ -91,6 +92,7 @@ export type Task = SPANavigationTask | MPANavigationTask
 // can be reused without initiating a server request.
 export function startPPRNavigation(
   navigatedAt: number,
+  oldUrl: URL,
   oldCacheNode: CacheNode,
   oldRouterState: FlightRouterState,
   newRouterState: FlightRouterState,
@@ -103,6 +105,7 @@ export function startPPRNavigation(
   const segmentPath: Array<FlightSegmentPath> = []
   return updateCacheNodeOnNavigation(
     navigatedAt,
+    oldUrl,
     oldCacheNode,
     oldRouterState,
     newRouterState,
@@ -118,6 +121,7 @@ export function startPPRNavigation(
 
 function updateCacheNodeOnNavigation(
   navigatedAt: number,
+  oldUrl: URL,
   oldCacheNode: CacheNode,
   oldRouterState: FlightRouterState,
   newRouterState: FlightRouterState,
@@ -230,7 +234,7 @@ function updateCacheNodeOnNavigation(
         // Reuse the existing Router State for this segment. We spawn a "task"
         // just to keep track of the updated router state; unlike most, it's
         // already fulfilled and won't be affected by the dynamic response.
-        taskChild = spawnReusedTask(oldRouterStateChild)
+        taskChild = reuseActiveSegmentInDefaultSlot(oldUrl, oldRouterStateChild)
       } else {
         // There's no currently active segment. Switch to the "create" path.
         taskChild = beginRenderingNewRouteTree(
@@ -299,6 +303,7 @@ function updateCacheNodeOnNavigation(
         // the children.
         taskChild = updateCacheNodeOnNavigation(
           navigatedAt,
+          oldUrl,
           oldCacheNodeChild,
           oldRouterStateChild,
           newRouterStateChild,
@@ -726,9 +731,37 @@ function spawnPendingTask(
   return newTask
 }
 
-function spawnReusedTask(reusedRouterState: FlightRouterState): Task {
-  // Create a task that reuses an existing segment, e.g. when reusing
-  // the current active segment in place of a default route.
+function reuseActiveSegmentInDefaultSlot(
+  oldUrl: URL,
+  oldRouterState: FlightRouterState
+): Task {
+  // This is a "default" segment. These are never sent by the server during a
+  // soft navigation; instead, the client reuses whatever segment was already
+  // active in that slot on the previous route. This means if we later need to
+  // refresh the segment, it will have to be refetched from the previous route's
+  // URL. We store it in the Flight Router State.
+  //
+  // TODO: We also mark the segment with a "refresh" marker but I think we can
+  // get rid of that eventually by making sure we only add URLs to page segments
+  // that are reused. Then the presence of the URL alone is enough.
+  let reusedRouterState
+
+  const oldRefreshMarker = oldRouterState[3]
+  if (oldRefreshMarker === 'refresh') {
+    // This segment was already reused from an even older route. Keep its
+    // existing URL and refresh marker.
+    reusedRouterState = oldRouterState
+  } else {
+    // This segment was not previously reused, and it's not on the new route.
+    // So it must have been delivered in the old route.
+    reusedRouterState = patchRouterStateWithNewChildren(
+      oldRouterState,
+      oldRouterState[1]
+    )
+    reusedRouterState[2] = createHrefFromUrl(oldUrl)
+    reusedRouterState[3] = 'refresh'
+  }
+
   return {
     route: reusedRouterState,
     node: null,

filename: packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts
patch: @@ -203,8 +203,10 @@ export function navigateReducer(
     // Temporary glue code between the router reducer and the new navigation
     // implementation. Eventually we'll rewrite the router reducer to a
     // state machine.
+    const currentUrl = new URL(state.canonicalUrl, location.origin)
     const result = navigateUsingSegmentCache(
       url,
+      currentUrl,
       state.cache,
       state.tree,
       state.nextUrl,
@@ -267,13 +269,14 @@ export function navigateReducer(
         return handleExternalUrl(state, mutable, flightData, pendingPush)
       }
 
+      const oldCanonicalUrl = state.canonicalUrl
       const updatedCanonicalUrl = canonicalUrlOverride
         ? createHrefFromUrl(canonicalUrlOverride)
         : href
 
       const onlyHashChange =
         !!hash &&
-        state.canonicalUrl.split('#', 1)[0] ===
+        oldCanonicalUrl.split('#', 1)[0] ===
           updatedCanonicalUrl.split('#', 1)[0]
 
       // If only the hash has changed, the server hasn't sent us any new data. We can just update
@@ -339,6 +342,7 @@ export function navigateReducer(
           ) {
             const task = startPPRNavigation(
               navigatedAt,
+              new URL(oldCanonicalUrl, url.origin),
               currentCache,
               currentTree,
               treePatch,

filename: packages/next/src/client/components/segment-cache-impl/navigation.ts
patch: @@ -77,6 +77,7 @@ export type NavigationResult =
  */
 export function navigate(
   url: URL,
+  currentUrl: URL,
   currentCacheNode: CacheNode,
   currentFlightRouterState: FlightRouterState,
   nextUrl: string | null,
@@ -124,6 +125,7 @@ export function navigate(
     return navigateUsingPrefetchedRouteTree(
       now,
       url,
+      currentUrl,
       nextUrl,
       isSamePageNavigation,
       currentCacheNode,
@@ -156,6 +158,7 @@ export function navigate(
     return navigateUsingPrefetchedRouteTree(
       now,
       url,
+      currentUrl,
       nextUrl,
       isSamePageNavigation,
       currentCacheNode,
@@ -176,6 +179,7 @@ export function navigate(
     data: navigateDynamicallyWithNoPrefetch(
       now,
       url,
+      currentUrl,
       nextUrl,
       isSamePageNavigation,
       currentCacheNode,
@@ -189,6 +193,7 @@ export function navigate(
 function navigateUsingPrefetchedRouteTree(
   now: number,
   url: URL,
+  currentUrl: URL,
   nextUrl: string | null,
   isSamePageNavigation: boolean,
   currentCacheNode: CacheNode,
@@ -210,6 +215,7 @@ function navigateUsingPrefetchedRouteTree(
   const scrollableSegments: Array<FlightSegmentPath> = []
   const task = startPPRNavigation(
     now,
+    currentUrl,
     currentCacheNode,
     currentFlightRouterState,
     prefetchFlightRouterState,
@@ -383,6 +389,7 @@ function readRenderSnapshotFromCache(
 async function navigateDynamicallyWithNoPrefetch(
   now: number,
   url: URL,
+  currentUrl: URL,
   nextUrl: string | null,
   isSamePageNavigation: boolean,
   currentCacheNode: CacheNode,
@@ -442,6 +449,7 @@ async function navigateDynamicallyWithNoPrefetch(
   const scrollableSegments: Array<FlightSegmentPath> = []
   const task = startPPRNavigation(
     now,
+    currentUrl,
     currentCacheNode,
     currentFlightRouterState,
     prefetchFlightRouterState,

filename: test/client-segment-cache-tests-manifest.json
patch: @@ -18,13 +18,6 @@
     },
     "test/e2e/app-dir/parallel-routes-revalidation/parallel-routes-revalidation.test.ts": {
       "failed": [
-        "parallel-routes-revalidation router.refresh (dynamic) - searchParams: false should correctly refresh data for previously intercepted modal and active page slot",
-        "parallel-routes-revalidation router.refresh (dynamic) - searchParams: true should correctly refresh data for previously intercepted modal and active page slot",
-        "parallel-routes-revalidation router.refresh (dynamic) - searchParams: true should correctly refresh data for the intercepted route and previously active page slot",
-        "parallel-routes-revalidation router.refresh (regular) - searchParams: false should correctly refresh data for previously intercepted modal and active page slot",
-        "parallel-routes-revalidation router.refresh (regular) - searchParams: true should correctly refresh data for previously intercepted modal and active page slot",
-        "parallel-routes-revalidation router.refresh (regular) - searchParams: true should correctly refresh data for the intercepted route and previously active page slot",
-        "parallel-routes-revalidation server action revalidation handles refreshing when multiple parallel slots are active",
         "parallel-routes-revalidation server action revalidation should not trigger a refresh for the page that is being redirected to"
       ]
     },