@@ -20,13 +20,17 @@ import type { FetchServerResponseResult } from './fetch-server-response'
20
20
// request. We can't use the Cache Node tree or Route State tree directly
21
21
// because those include reused nodes, too. This tree is discarded as soon as
22
22
// the navigation response is received.
23
- type Task = {
23
+ export type Task = {
24
24
// The router state that corresponds to the tree that this Task represents.
25
25
route : FlightRouterState
26
- // This is usually non-null. It represents a brand new Cache Node tree whose
27
- // data is still pending. If it's null, it means there's no pending data but
28
- // the client patched the router state.
26
+ // The CacheNode that corresponds to the tree that this Task represents. If
27
+ // `children` is null (i.e. if this is a terminal task node), then `node`
28
+ // represents a brand new Cache Node tree, which way or may not need to be
29
+ // filled with dynamic data from the server.
29
30
node : CacheNode | null
31
+ // Whether anything in this tree contains dynamic holes that need to be filled
32
+ // by the server.
33
+ needsDynamicRequest : boolean
30
34
children : Map < string , Task > | null
31
35
}
32
36
@@ -64,7 +68,8 @@ export function updateCacheNodeOnNavigation(
64
68
oldRouterState : FlightRouterState ,
65
69
newRouterState : FlightRouterState ,
66
70
prefetchData : CacheNodeSeedData | null ,
67
- prefetchHead : React . ReactNode | null
71
+ prefetchHead : React . ReactNode | null ,
72
+ isPrefetchHeadPartial : boolean
68
73
) : Task | null {
69
74
// Diff the old and new trees to reuse the shared layouts.
70
75
const oldRouterStateChildren = oldRouterState [ 1 ]
@@ -96,14 +101,15 @@ export function updateCacheNodeOnNavigation(
96
101
} = { }
97
102
let taskChildren = null
98
103
99
- // For most navigations, we need to issue a "dynamic" request to fetch the
100
- // full RSC data from the server since during rendering, we'll only serve
101
- // the prefetch shell. For some navigations, we re-use the existing cache node
102
- // (via `spawnReusedTask`), and don't actually need fresh data from the server.
103
- // In those cases, we use this `needsDynamicRequest` flag to return a `null`
104
- // cache node, which signals to the caller that we don't need to issue a
105
- // dynamic request. We start off with a `false` value, and then for each parallel
106
- // route, we set it to `true` if we encounter a segment that needs a dynamic request.
104
+ // Most navigations require a request to fetch additional data from the
105
+ // server, either because the data was not already prefetched, or because the
106
+ // target route contains dynamic data that cannot be prefetched.
107
+ //
108
+ // However, if the target route is fully static, and it's already completely
109
+ // loaded into the segment cache, then we can skip the server request.
110
+ //
111
+ // This starts off as `false`, and is set to `true` if any of the child
112
+ // routes requires a dynamic request.
107
113
let needsDynamicRequest = false
108
114
109
115
for ( let parallelRouteKey in newRouterStateChildren ) {
@@ -144,10 +150,11 @@ export function updateCacheNodeOnNavigation(
144
150
taskChild = spawnReusedTask ( oldRouterStateChild )
145
151
} else {
146
152
// There's no currently active segment. Switch to the "create" path.
147
- taskChild = spawnPendingTask (
153
+ taskChild = createCacheNodeOnNavigation (
148
154
newRouterStateChild ,
149
155
prefetchDataChild !== undefined ? prefetchDataChild : null ,
150
- prefetchHead
156
+ prefetchHead ,
157
+ isPrefetchHeadPartial
151
158
)
152
159
}
153
160
} else if (
@@ -165,24 +172,27 @@ export function updateCacheNodeOnNavigation(
165
172
oldRouterStateChild ,
166
173
newRouterStateChild ,
167
174
prefetchDataChild ,
168
- prefetchHead
175
+ prefetchHead ,
176
+ isPrefetchHeadPartial
169
177
)
170
178
} else {
171
179
// Either there's no existing Cache Node for this segment, or this
172
180
// segment doesn't exist in the old Router State tree. Switch to the
173
181
// "create" path.
174
- taskChild = spawnPendingTask (
182
+ taskChild = createCacheNodeOnNavigation (
175
183
newRouterStateChild ,
176
184
prefetchDataChild !== undefined ? prefetchDataChild : null ,
177
- prefetchHead
185
+ prefetchHead ,
186
+ isPrefetchHeadPartial
178
187
)
179
188
}
180
189
} else {
181
190
// This is a new tree. Switch to the "create" path.
182
- taskChild = spawnPendingTask (
191
+ taskChild = createCacheNodeOnNavigation (
183
192
newRouterStateChild ,
184
193
prefetchDataChild !== undefined ? prefetchDataChild : null ,
185
- prefetchHead
194
+ prefetchHead ,
195
+ isPrefetchHeadPartial
186
196
)
187
197
}
188
198
@@ -197,8 +207,9 @@ export function updateCacheNodeOnNavigation(
197
207
const newSegmentMapChild : ChildSegmentMap = new Map ( oldSegmentMapChild )
198
208
newSegmentMapChild . set ( newSegmentKeyChild , newCacheNodeChild )
199
209
prefetchParallelRoutes . set ( parallelRouteKey , newSegmentMapChild )
200
- // a non-null taskChild.node means we're waiting for a dynamic request to
201
- // fill in the missing data
210
+ }
211
+
212
+ if ( taskChild . needsDynamicRequest ) {
202
213
needsDynamicRequest = true
203
214
}
204
215
@@ -241,9 +252,110 @@ export function updateCacheNodeOnNavigation(
241
252
newRouterState ,
242
253
patchedRouterStateChildren
243
254
) ,
244
- // Only return the new cache node if there are pending tasks that need to be resolved
245
- // by the dynamic data from the server. If they don't, we don't need to trigger a dynamic request.
246
- node : needsDynamicRequest ? newCacheNode : null ,
255
+ node : newCacheNode ,
256
+ needsDynamicRequest,
257
+ children : taskChildren ,
258
+ }
259
+ }
260
+
261
+ function createCacheNodeOnNavigation (
262
+ routerState : FlightRouterState ,
263
+ prefetchData : CacheNodeSeedData | null ,
264
+ possiblyPartialPrefetchHead : React . ReactNode | null ,
265
+ isPrefetchHeadPartial : boolean
266
+ ) : Task {
267
+ // Same traversal as updateCacheNodeNavigation, but we switch to this path
268
+ // once we reach the part of the tree that was not in the previous route. We
269
+ // don't need to diff against the old tree, we just need to create a new one.
270
+ if ( prefetchData === null ) {
271
+ // There's no prefetch for this segment. Everything from this point will be
272
+ // requested from the server, even if there are static children below it.
273
+ // Create a terminal task node that will later be fulfilled by
274
+ // server response.
275
+ return spawnPendingTask (
276
+ routerState ,
277
+ null ,
278
+ possiblyPartialPrefetchHead ,
279
+ isPrefetchHeadPartial
280
+ )
281
+ }
282
+
283
+ const routerStateChildren = routerState [ 1 ]
284
+ const isPrefetchRscPartial = prefetchData [ 4 ]
285
+
286
+ // The head is assigned to every leaf segment delivered by the server. Based
287
+ // on corresponding logic in fill-lazy-items-till-leaf-with-head.ts
288
+ const isLeafSegment = Object . keys ( routerStateChildren ) . length === 0
289
+
290
+ // If prefetch data is available for a segment, and it's fully static (i.e.
291
+ // does not contain any dynamic holes), we don't need to request it from
292
+ // the server.
293
+ if (
294
+ // Check if the segment data is partial
295
+ isPrefetchRscPartial ||
296
+ // Check if the head is partial (only relevant if this is a leaf segment)
297
+ ( isPrefetchHeadPartial && isLeafSegment )
298
+ ) {
299
+ // We only have partial data from this segment. Like missing segments, we
300
+ // must request the full data from the server.
301
+ return spawnPendingTask (
302
+ routerState ,
303
+ prefetchData ,
304
+ possiblyPartialPrefetchHead ,
305
+ isPrefetchHeadPartial
306
+ )
307
+ }
308
+
309
+ // The prefetched segment is fully static, so we don't need to request a new
310
+ // one from the server. Keep traversing down the tree until we reach something
311
+ // that requires a dynamic request.
312
+ const prefetchDataChildren = prefetchData [ 2 ]
313
+ const taskChildren = new Map ( )
314
+ const cacheNodeChildren = new Map ( )
315
+ let needsDynamicRequest = false
316
+ for ( let parallelRouteKey in routerStateChildren ) {
317
+ const routerStateChild : FlightRouterState =
318
+ routerStateChildren [ parallelRouteKey ]
319
+ const prefetchDataChild : CacheNodeSeedData | void | null =
320
+ prefetchDataChildren !== null
321
+ ? prefetchDataChildren [ parallelRouteKey ]
322
+ : null
323
+ const segmentChild = routerStateChild [ 0 ]
324
+ const segmentKeyChild = createRouterCacheKey ( segmentChild )
325
+ const taskChild = createCacheNodeOnNavigation (
326
+ routerStateChild ,
327
+ prefetchDataChild ,
328
+ possiblyPartialPrefetchHead ,
329
+ isPrefetchHeadPartial
330
+ )
331
+ taskChildren . set ( parallelRouteKey , taskChild )
332
+ if ( taskChild . needsDynamicRequest ) {
333
+ needsDynamicRequest = true
334
+ }
335
+ const newCacheNodeChild = taskChild . node
336
+ if ( newCacheNodeChild !== null ) {
337
+ const newSegmentMapChild : ChildSegmentMap = new Map ( )
338
+ newSegmentMapChild . set ( segmentKeyChild , newCacheNodeChild )
339
+ cacheNodeChildren . set ( parallelRouteKey , newSegmentMapChild )
340
+ }
341
+ }
342
+
343
+ const rsc = prefetchData [ 1 ]
344
+ const loading = prefetchData [ 3 ]
345
+ return {
346
+ route : routerState ,
347
+ node : {
348
+ lazyData : null ,
349
+ // Since this is a fully static segment, we don't need to use the
350
+ // `prefetchRsc` field.
351
+ rsc,
352
+ prefetchRsc : null ,
353
+ head : isLeafSegment ? possiblyPartialPrefetchHead : null ,
354
+ prefetchHead : null ,
355
+ loading,
356
+ parallelRoutes : cacheNodeChildren ,
357
+ } ,
358
+ needsDynamicRequest,
247
359
children : taskChildren ,
248
360
}
249
361
}
@@ -271,19 +383,26 @@ function patchRouterStateWithNewChildren(
271
383
function spawnPendingTask (
272
384
routerState : FlightRouterState ,
273
385
prefetchData : CacheNodeSeedData | null ,
274
- prefetchHead : React . ReactNode | null
386
+ prefetchHead : React . ReactNode | null ,
387
+ isPrefetchHeadPartial : boolean
275
388
) : Task {
276
389
// Create a task that will later be fulfilled by data from the server.
277
- const pendingCacheNode = createPendingCacheNode (
278
- routerState ,
279
- prefetchData ,
280
- prefetchHead
281
- )
282
- return {
390
+ const newTask : Task = {
283
391
route : routerState ,
284
- node : pendingCacheNode ,
392
+
393
+ // Corresponds to the part of the route that will be rendered on the server.
394
+ node : createPendingCacheNode (
395
+ routerState ,
396
+ prefetchData ,
397
+ prefetchHead ,
398
+ isPrefetchHeadPartial
399
+ ) ,
400
+ // Set this to true to indicate that this tree is missing data. This will
401
+ // be propagated to all the parent tasks.
402
+ needsDynamicRequest : true ,
285
403
children : null ,
286
404
}
405
+ return newTask
287
406
}
288
407
289
408
function spawnReusedTask ( reusedRouterState : FlightRouterState ) : Task {
@@ -292,6 +411,7 @@ function spawnReusedTask(reusedRouterState: FlightRouterState): Task {
292
411
return {
293
412
route : reusedRouterState ,
294
413
node : null ,
414
+ needsDynamicRequest : false ,
295
415
children : null ,
296
416
}
297
417
}
@@ -413,6 +533,11 @@ function finishTaskUsingDynamicDataPayload(
413
533
dynamicData : CacheNodeSeedData ,
414
534
dynamicHead : React . ReactNode
415
535
) {
536
+ if ( ! task . needsDynamicRequest ) {
537
+ // Everything in this subtree is already complete. Bail out.
538
+ return
539
+ }
540
+
416
541
// dynamicData may represent a larger subtree than the task. Before we can
417
542
// finish the task, we need to line them up.
418
543
const taskChildren = task . children
@@ -429,8 +554,8 @@ function finishTaskUsingDynamicDataPayload(
429
554
dynamicData ,
430
555
dynamicHead
431
556
)
432
- // Null this out to indicate that the task is complete.
433
- task . node = null
557
+ // Set this to false to indicate that this task is now complete.
558
+ task . needsDynamicRequest = false
434
559
}
435
560
return
436
561
}
@@ -472,7 +597,8 @@ function finishTaskUsingDynamicDataPayload(
472
597
function createPendingCacheNode (
473
598
routerState : FlightRouterState ,
474
599
prefetchData : CacheNodeSeedData | null ,
475
- prefetchHead : React . ReactNode | null
600
+ prefetchHead : React . ReactNode | null ,
601
+ isPrefetchHeadPartial : boolean
476
602
) : ReadyCacheNode {
477
603
const routerStateChildren = routerState [ 1 ]
478
604
const prefetchDataChildren = prefetchData !== null ? prefetchData [ 2 ] : null
@@ -492,7 +618,8 @@ function createPendingCacheNode(
492
618
const newCacheNodeChild = createPendingCacheNode (
493
619
routerStateChild ,
494
620
prefetchDataChild === undefined ? null : prefetchDataChild ,
495
- prefetchHead
621
+ prefetchHead ,
622
+ isPrefetchHeadPartial
496
623
)
497
624
498
625
const newSegmentMapChild : ChildSegmentMap = new Map ( )
@@ -503,7 +630,6 @@ function createPendingCacheNode(
503
630
// The head is assigned to every leaf segment delivered by the server. Based
504
631
// on corresponding logic in fill-lazy-items-till-leaf-with-head.ts
505
632
const isLeafSegment = parallelRoutes . size === 0
506
-
507
633
const maybePrefetchRsc = prefetchData !== null ? prefetchData [ 1 ] : null
508
634
const maybePrefetchLoading = prefetchData !== null ? prefetchData [ 3 ] : null
509
635
return {
@@ -512,6 +638,10 @@ function createPendingCacheNode(
512
638
513
639
prefetchRsc : maybePrefetchRsc !== undefined ? maybePrefetchRsc : null ,
514
640
prefetchHead : isLeafSegment ? prefetchHead : null ,
641
+
642
+ // TODO: Technically, a loading boundary could contain dynamic data. We must
643
+ // have separate `loading` and `prefetchLoading` fields to handle this, like
644
+ // we do for the segment data and head.
515
645
loading : maybePrefetchLoading !== undefined ? maybePrefetchLoading : null ,
516
646
517
647
// Create a deferred promise. This will be fulfilled once the dynamic
@@ -645,8 +775,8 @@ export function abortTask(task: Task, error: any): void {
645
775
}
646
776
}
647
777
648
- // Null this out to indicate that the task is complete.
649
- task . node = null
778
+ // Set this to false to indicate that this task is now complete.
779
+ task . needsDynamicRequest = false
650
780
}
651
781
652
782
function abortPendingCacheNode (
0 commit comments