Skip to content

Commit 69c203c

Browse files
committed
Re-implement Server Action reducer
Rewrite of the Server Action reducer to use the PPR/Segment Cache navigation implementation, rather than the old lazy fetch implementation. Server Actions may trigger a revalidation, a redirect, or both. They may also invalidate the cache. The behavior could be naively implemented using router.refresh() and router.push(). Semantically, the routing behavior is equivalent. The main difference is that the server that invokes the action may also send back new data for the page within the same response. Compared to a separate request, this data is more likely to be consistent with any data that may have been mutated by the action, due to global data propagation races. (It's also faster since it avoids an extra server waterfall.) So, navigations initiated by a Server action must be able to "seed" the navigation with the data it just received from the server. I've added a new internal method, navigateToSeededRoute, that implements this behavior.
1 parent 4e663c1 commit 69c203c

File tree

7 files changed

+347
-213
lines changed

7 files changed

+347
-213
lines changed

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

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export type NavigationTask = {
5151
}
5252

5353
export type NavigationRequestAccumulation = {
54-
scrollableSegments: Array<FlightSegmentPath>
54+
scrollableSegments: Array<FlightSegmentPath> | null
5555
separateRefreshUrls: Set<string> | null
5656
}
5757

@@ -91,6 +91,8 @@ export function startPPRNavigation(
9191
oldRouterState: FlightRouterState,
9292
newRouterState: FlightRouterState,
9393
shouldRefreshDynamicData: boolean,
94+
seedData: CacheNodeSeedData | null,
95+
seedHead: HeadData | null,
9496
prefetchData: CacheNodeSeedData | null,
9597
prefetchHead: HeadData | null,
9698
isPrefetchHeadPartial: boolean,
@@ -108,6 +110,8 @@ export function startPPRNavigation(
108110
newRouterState,
109111
shouldRefreshDynamicData,
110112
didFindRootLayout,
113+
seedData,
114+
seedHead,
111115
prefetchData,
112116
prefetchHead,
113117
isPrefetchHeadPartial,
@@ -128,6 +132,8 @@ function updateCacheNodeOnNavigation(
128132
newRouterState: FlightRouterState,
129133
shouldRefreshDynamicData: boolean,
130134
didFindRootLayout: boolean,
135+
seedData: CacheNodeSeedData | null,
136+
seedHead: HeadData | null,
131137
prefetchData: CacheNodeSeedData | null,
132138
prefetchHead: HeadData | null,
133139
isPrefetchHeadPartial: boolean,
@@ -193,6 +199,8 @@ function updateCacheNodeOnNavigation(
193199
newRouterState,
194200
oldCacheNode,
195201
shouldRefreshDynamicData,
202+
seedData,
203+
seedHead,
196204
prefetchData,
197205
prefetchHead,
198206
isPrefetchHeadPartial,
@@ -216,6 +224,7 @@ function updateCacheNodeOnNavigation(
216224

217225
const newRouterStateChildren = newRouterState[1]
218226
const oldRouterStateChildren = oldRouterState[1]
227+
const seedDataChildren = seedData !== null ? seedData[1] : null
219228
const prefetchDataChildren = prefetchData !== null ? prefetchData[1] : null
220229

221230
// We're currently traversing the part of the tree that was also part of
@@ -264,6 +273,28 @@ function updateCacheNodeOnNavigation(
264273
// Reuse the existing CacheNode
265274
newCacheNode = reuseDynamicCacheNode(oldCacheNode, newParallelRoutes)
266275
needsDynamicRequest = false
276+
} else if (seedData !== null) {
277+
// If this navigation was the result of an action, then check if the
278+
// server sent back data in the action response. We should favor using
279+
// that, rather than performing a separate request. This is both better
280+
// for performance and it's more likely to be consistent with any
281+
// writes that were just performed by the action, compared to a
282+
// separate request.
283+
const seedRsc = seedData[0]
284+
const seedLoading = seedData[2]
285+
const isSeedRscPartial = false
286+
const isSeedHeadPartial = seedHead === null
287+
newCacheNode = readCacheNodeFromSeedData(
288+
seedRsc,
289+
seedLoading,
290+
isSeedRscPartial,
291+
seedHead,
292+
isSeedHeadPartial,
293+
isLeafSegment,
294+
newParallelRoutes,
295+
navigatedAt
296+
)
297+
needsDynamicRequest = isLeafSegment && isSeedHeadPartial
267298
} else if (prefetchData !== null) {
268299
// Consult the prefetch cache.
269300
const prefetchRsc = prefetchData[0]
@@ -357,12 +388,16 @@ function updateCacheNodeOnNavigation(
357388
oldParallelRoutes !== undefined
358389
? oldParallelRoutes.get(parallelRouteKey)
359390
: undefined
391+
392+
let seedDataChild: CacheNodeSeedData | void | null =
393+
seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null
360394
let prefetchDataChild: CacheNodeSeedData | void | null =
361395
prefetchDataChildren !== null
362396
? prefetchDataChildren[parallelRouteKey]
363397
: null
364398

365399
let newSegmentChild = newRouterStateChild[0]
400+
let seedHeadChild = seedHead
366401
let prefetchHeadChild = prefetchHead
367402
let isPrefetchHeadPartialChild = isPrefetchHeadPartial
368403
if (newSegmentChild === DEFAULT_SEGMENT_KEY) {
@@ -377,6 +412,8 @@ function updateCacheNodeOnNavigation(
377412

378413
// Since we're switching to a different route tree, these are no
379414
// longer valid, because they correspond to the outer tree.
415+
seedDataChild = null
416+
seedHeadChild = null
380417
prefetchDataChild = null
381418
prefetchHeadChild = null
382419
isPrefetchHeadPartialChild = false
@@ -396,6 +433,8 @@ function updateCacheNodeOnNavigation(
396433
newRouterStateChild,
397434
shouldRefreshDynamicData,
398435
childDidFindRootLayout,
436+
seedDataChild ?? null,
437+
seedHeadChild,
399438
prefetchDataChild ?? null,
400439
prefetchHeadChild,
401440
isPrefetchHeadPartialChild,
@@ -421,7 +460,9 @@ function updateCacheNodeOnNavigation(
421460
taskChildren.set(parallelRouteKey, taskChild)
422461
const newCacheNodeChild = taskChild.node
423462
if (newCacheNodeChild !== null) {
424-
const newSegmentMapChild: ChildSegmentMap = new Map(oldSegmentMapChild)
463+
const newSegmentMapChild: ChildSegmentMap = new Map(
464+
shouldRefreshDynamicData ? undefined : oldSegmentMapChild
465+
)
425466
newSegmentMapChild.set(newSegmentKeyChild, newCacheNodeChild)
426467
newParallelRoutes.set(parallelRouteKey, newSegmentMapChild)
427468
}
@@ -471,6 +512,8 @@ function createCacheNodeOnNavigation(
471512
newRouterState: FlightRouterState,
472513
oldCacheNode: CacheNode | void,
473514
shouldRefreshDynamicData: boolean,
515+
seedData: CacheNodeSeedData | null,
516+
seedHead: HeadData | null,
474517
prefetchData: CacheNodeSeedData | null,
475518
prefetchHead: HeadData | null,
476519
isPrefetchHeadPartial: boolean,
@@ -497,6 +540,7 @@ function createCacheNodeOnNavigation(
497540

498541
const newRouterStateChildren = newRouterState[1]
499542
const prefetchDataChildren = prefetchData !== null ? prefetchData[1] : null
543+
const seedDataChildren = seedData !== null ? seedData[1] : null
500544
const oldParallelRoutes =
501545
oldCacheNode !== undefined ? oldCacheNode.parallelRoutes : undefined
502546
const newParallelRoutes = new Map(
@@ -514,6 +558,9 @@ function createCacheNodeOnNavigation(
514558
// TODO: We should use a string to represent the segment path instead of
515559
// an array. We already use a string representation for the path when
516560
// accessing the Segment Cache, so we can use the same one.
561+
if (accumulation.scrollableSegments === null) {
562+
accumulation.scrollableSegments = []
563+
}
517564
accumulation.scrollableSegments.push(segmentPath)
518565
}
519566

@@ -534,6 +581,28 @@ function createCacheNodeOnNavigation(
534581
// Reuse the existing CacheNode
535582
newCacheNode = reuseDynamicCacheNode(oldCacheNode, newParallelRoutes)
536583
needsDynamicRequest = false
584+
} else if (seedData !== null) {
585+
// If this navigation was the result of an action, then check if the
586+
// server sent back data in the action response. We should favor using
587+
// that, rather than performing a separate request. This is both better
588+
// for performance and it's more likely to be consistent with any
589+
// writes that were just performed by the action, compared to a
590+
// separate request.
591+
const seedRsc = seedData[0]
592+
const seedLoading = seedData[2]
593+
const isSeedRscPartial = false
594+
const isSeedHeadPartial = seedHead === null
595+
newCacheNode = readCacheNodeFromSeedData(
596+
seedRsc,
597+
seedLoading,
598+
isSeedRscPartial,
599+
seedHead,
600+
isSeedHeadPartial,
601+
isLeafSegment,
602+
newParallelRoutes,
603+
navigatedAt
604+
)
605+
needsDynamicRequest = isLeafSegment && isSeedHeadPartial
537606
} else if (prefetchData !== null) {
538607
// Consult the prefetch cache.
539608
const prefetchRsc = prefetchData[0]
@@ -578,6 +647,8 @@ function createCacheNodeOnNavigation(
578647
oldParallelRoutes !== undefined
579648
? oldParallelRoutes.get(parallelRouteKey)
580649
: undefined
650+
const seedDataChild: CacheNodeSeedData | void | null =
651+
seedDataChildren !== null ? seedDataChildren[parallelRouteKey] : null
581652
const prefetchDataChild: CacheNodeSeedData | void | null =
582653
prefetchDataChildren !== null
583654
? prefetchDataChildren[parallelRouteKey]
@@ -596,6 +667,8 @@ function createCacheNodeOnNavigation(
596667
newRouterStateChild,
597668
oldCacheNodeChild,
598669
shouldRefreshDynamicData,
670+
seedDataChild ?? null,
671+
seedHead,
599672
prefetchDataChild ?? null,
600673
prefetchHead,
601674
isPrefetchHeadPartial,
@@ -611,7 +684,9 @@ function createCacheNodeOnNavigation(
611684
taskChildren.set(parallelRouteKey, taskChild)
612685
const newCacheNodeChild = taskChild.node
613686
if (newCacheNodeChild !== null) {
614-
const newSegmentMapChild: ChildSegmentMap = new Map(oldSegmentMapChild)
687+
const newSegmentMapChild: ChildSegmentMap = new Map(
688+
shouldRefreshDynamicData ? undefined : oldSegmentMapChild
689+
)
615690
newSegmentMapChild.set(newSegmentKeyChild, newCacheNodeChild)
616691
newParallelRoutes.set(parallelRouteKey, newSegmentMapChild)
617692
}

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
@@ -161,12 +161,14 @@ export function navigateReducer(
161161
// implementation. Eventually we'll rewrite the router reducer to a
162162
// state machine.
163163
const currentUrl = new URL(state.canonicalUrl, location.origin)
164+
const shouldRefreshDynamicData = false
164165
const result = navigateUsingSegmentCache(
165166
url,
166167
currentUrl,
167168
state.cache,
168169
state.tree,
169170
state.nextUrl,
171+
shouldRefreshDynamicData,
170172
shouldScroll,
171173
mutable
172174
)

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

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import type {
55
RefreshAction,
66
} from '../router-reducer-types'
77
import { handleNavigationResult } from './navigate-reducer'
8-
import { refresh as refreshUsingSegmentCache } from '../../segment-cache/navigation'
8+
import { navigateToSeededRoute } from '../../segment-cache/navigation'
99
import { revalidateEntireCache } from '../../segment-cache/cache'
10+
import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree'
1011

1112
export function refreshReducer(
1213
state: ReadonlyReducerState,
@@ -19,14 +20,38 @@ export function refreshReducer(
1920
const currentRouterState = state.tree
2021
revalidateEntireCache(currentNextUrl, currentRouterState)
2122

23+
// We always send the last next-url, not the current when performing a dynamic
24+
// request. This is because we update the next-url after a navigation, but we
25+
// want the same interception route to be matched that used the last next-url.
26+
const nextUrlForRefresh = hasInterceptionRouteInCurrentTree(state.tree)
27+
? state.previousNextUrl || currentNextUrl
28+
: null
29+
30+
// A refresh is modeled as a navigation to the current URL, but where any
31+
// existing dynamic data (including in shared layouts) is re-fetched.
2232
const currentUrl = new URL(state.canonicalUrl, action.origin)
23-
const result = refreshUsingSegmentCache(
33+
const url = currentUrl
34+
const currentFlightRouterState = state.tree
35+
const shouldScroll = true
36+
const shouldRefreshDynamicData = true
37+
38+
const seedFlightRouterState = state.tree
39+
const seedRenderedSearch = state.renderedSearch
40+
const seedData = null
41+
const seedHead = null
42+
43+
const result = navigateToSeededRoute(
44+
url,
2445
currentUrl,
2546
state.cache,
26-
state.tree,
27-
state.nextUrl,
28-
state.renderedSearch,
29-
state.canonicalUrl
47+
currentFlightRouterState,
48+
seedFlightRouterState,
49+
seedRenderedSearch,
50+
seedData,
51+
seedHead,
52+
shouldRefreshDynamicData,
53+
nextUrlForRefresh,
54+
shouldScroll
3055
)
3156

3257
const mutable: Mutable = {}

0 commit comments

Comments
 (0)