@@ -316,130 +316,145 @@ async function singleFetchLoaderNavigationStrategy(
316
316
matches : DataStrategyFunctionArgs [ "matches" ] ,
317
317
basename : string | undefined
318
318
) {
319
- // Routes that need to be loaded from the server
320
- let serverLoaderRouteDfds = new Map < string , Deferred > ( ) ;
319
+ // Track which routes need a server load - in case we need to tack on a
320
+ // `_routes` param
321
+ let routesParams = new Set < string > ( ) ;
321
322
322
- // Did any routes opt-out of a server loader via`shouldRevalidate`/`clientLoader`?
323
+ // We only add `_routes` when one or more routes opts out of a load via
324
+ // `shouldRevalidate` or `clientLoader`
323
325
let foundOptOutRoute = false ;
324
326
325
- // Deferreds to ensure all routes have loaded via match.resolve()
327
+ // Deferreds for each route so we can be sure they've all loaded via
328
+ // `match.resolve()`, and a singular promise that can tell us all routes
329
+ // have been resolved
326
330
let routeDfds = matches . map ( ( ) => createDeferred < void > ( ) ) ;
331
+ let routesLoadedPromise = Promise . all ( routeDfds . map ( ( d ) => d . promise ) ) ;
327
332
328
- // We'll build up this results object as we loop through matches
329
- let results : Record < string , DataStrategyResult > = { } ;
333
+ // Deferred that we'll use for the call to the server that each match can
334
+ // await and parse out it's specific result
335
+ let singleFetchDfd = createDeferred < SingleFetchResults > ( ) ;
330
336
331
- // Base URL/ RequestInit for calls to the server
337
+ // Base URL and RequestInit for calls to the server
332
338
let url = stripIndexParam ( singleFetchUrl ( request . url , basename ) ) ;
333
339
let init = await createRequestInit ( request ) ;
334
340
341
+ // We'll build up this results object as we loop through matches
342
+ let results : Record < string , DataStrategyResult > = { } ;
343
+
335
344
let resolvePromise = Promise . all (
336
345
matches . map ( async ( m , i ) =>
337
346
m . resolve ( async ( handler ) => {
338
347
routeDfds [ i ] . resolve ( ) ;
339
348
340
- let { id : routeId , shouldRevalidate } = m . route ;
341
- let manifestRoute = manifest . routes [ routeId ] ;
342
- invariant ( manifestRoute , "No manifest route found for dataStrategy" ) ;
343
- let { hasLoader, hasClientLoader } = manifestRoute ;
344
-
345
- // Short circuit if there's no loader to call. Give this route an
346
- // `undefined` result because we always have a `loader` on the client
347
- // route in framework mode (for loading module/styles) so this ensures
348
- // it doesn't think it's waiting for data and trigger `HydrateFallback`
349
- if ( ! hasLoader && ! hasClientLoader && ! window . __reactRouterHdrActive ) {
350
- results [ routeId ] = { type : "data" , result : undefined } ;
351
- return ;
352
- }
353
-
354
- // Respect `shouldLoad` on initial load because we're only dealing with
355
- // `clientLoader.hydrate` routes which will fall into the `clientLoader`
356
- // section below.
357
- if ( ! m . shouldLoad && ! router . state . initialized ) {
358
- return ;
359
- }
349
+ let manifestRoute = manifest . routes [ m . route . id ] ;
350
+
351
+ // Note: If this logic changes for routes that should not participate
352
+ // in Single Fetch, make sure you update getLowestLoadingIndex above
353
+ // as well
354
+ if ( ! m . shouldLoad ) {
355
+ // If we're not yet initialized and this is the initial load, respect
356
+ // `shouldLoad` because we're only dealing with `clientLoader.hydrate`
357
+ // routes which will fall into the `clientLoader` section below.
358
+ if ( ! router . state . initialized ) {
359
+ return ;
360
+ }
360
361
361
- // Otherwise, skip the call if we currently have data and a `shouldRevalidate`
362
- // function - which implies that the user opted out via `shouldRevalidate`
363
- if (
364
- ! m . shouldLoad &&
365
- routeId in router . state . loaderData &&
366
- shouldRevalidate
367
- ) {
368
- // If a server loader exists, ensure it's excluded from the .data request
369
- if ( hasLoader ) {
370
- foundOptOutRoute = true ;
362
+ // Otherwise, we opt out if we currently have data and a
363
+ // `shouldRevalidate` function. This implies that the user opted out
364
+ // via `shouldRevalidate`
365
+ if (
366
+ m . route . id in router . state . loaderData &&
367
+ manifestRoute &&
368
+ m . route . shouldRevalidate
369
+ ) {
370
+ if ( manifestRoute . hasLoader ) {
371
+ // If we have a server loader, make sure we don't include it in the
372
+ // single fetch .data request
373
+ foundOptOutRoute = true ;
374
+ }
375
+ return ;
371
376
}
372
- return ;
373
377
}
374
378
375
379
// When a route has a client loader, it opts out of the singular call and
376
380
// calls it's server loader via `serverLoader()` using a `?_routes` param
377
- if ( hasClientLoader ) {
378
- if ( hasLoader ) {
381
+ if ( manifestRoute && manifestRoute . hasClientLoader ) {
382
+ if ( manifestRoute . hasLoader ) {
379
383
foundOptOutRoute = true ;
380
384
}
381
385
try {
382
- let result = await fetchSingleLoader ( handler , url , init , routeId ) ;
383
- results [ routeId ] = { type : "data" , result } ;
386
+ let result = await fetchSingleLoader (
387
+ handler ,
388
+ url ,
389
+ init ,
390
+ m . route . id
391
+ ) ;
392
+ results [ m . route . id ] = { type : "data" , result } ;
384
393
} catch ( e ) {
385
- results [ routeId ] = { type : "error" , result : e } ;
394
+ results [ m . route . id ] = { type : "error" , result : e } ;
386
395
}
387
396
return ;
388
397
}
389
398
390
- let dfd = createDeferred ( ) ;
391
- serverLoaderRouteDfds . set ( routeId , dfd ) ;
392
- await handler ( ( ) => dfd . promise ) ;
399
+ // Load this route on the server if it has a loader
400
+ if ( manifestRoute && manifestRoute . hasLoader ) {
401
+ routesParams . add ( m . route . id ) ;
402
+ }
403
+
404
+ // Lump this match in with the others on a singular promise
405
+ try {
406
+ let result = await handler ( async ( ) => {
407
+ let data = await singleFetchDfd . promise ;
408
+ return unwrapSingleFetchResults ( data , m . route . id ) ;
409
+ } ) ;
410
+ results [ m . route . id ] = {
411
+ type : "data" ,
412
+ result,
413
+ } ;
414
+ } catch ( e ) {
415
+ results [ m . route . id ] = {
416
+ type : "error" ,
417
+ result : e ,
418
+ } ;
419
+ }
393
420
} )
394
421
)
395
422
) ;
396
423
397
424
// Wait for all routes to resolve above before we make the HTTP call
398
- await Promise . all ( routeDfds . map ( ( d ) => d . promise ) ) ;
425
+ await routesLoadedPromise ;
399
426
400
- // Only make the server call if:
401
- // - We have loaders on the server that need fetching
402
- // - We're already hydrated - during initial hydration only clientLoaders can
403
- // run (via `clientLoader.hydrate`) which are handled above
427
+ // We can skip the server call:
428
+ // - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate`
429
+ // - If there are no routes to fetch from the server
404
430
//
405
431
// One exception - if we are performing an HDR revalidation we have to call
406
432
// the server in case a new loader has shown up that the manifest doesn't yet
407
433
// know about
408
434
if (
409
- ( serverLoaderRouteDfds . size > 0 && router . state . initialized ) ||
410
- window . __reactRouterHdrActive
435
+ ( ! router . state . initialized || routesParams . size === 0 ) &&
436
+ ! window . __reactRouterHdrActive
411
437
) {
412
- // When routes have opted out, add a `_routes` param to filter server loaders
413
- // Skipped in `ssr:false` because we expect to be loading static `.data` files
414
- if ( ssr && foundOptOutRoute && serverLoaderRouteDfds . size > 0 ) {
415
- let routes = [ ...serverLoaderRouteDfds . keys ( ) ] . join ( "," ) ;
416
- url . searchParams . set ( "_routes" , routes ) ;
417
- }
418
-
438
+ singleFetchDfd . resolve ( { } ) ;
439
+ } else {
419
440
try {
420
- let { data } = ( await fetchAndDecode ( url , init ) ) as {
421
- data : SingleFetchResults ;
422
- } ;
423
- for ( let [ routeId , dfd ] of serverLoaderRouteDfds ) {
424
- try {
425
- results [ routeId ] = {
426
- type : "data" ,
427
- result : unwrapSingleFetchResults ( data , routeId ) ,
428
- } ;
429
- dfd . resolve ( results [ routeId ] ) ;
430
- } catch ( e ) {
431
- results [ routeId ] = { type : "error" , result : e } ;
432
- dfd . reject ( results [ routeId ] ) ;
433
- }
441
+ // When one or more routes have opted out, we add a _routes param to
442
+ // limit the loaders to those that have a server loader and did not
443
+ // opt out
444
+ if ( ssr && foundOptOutRoute && routesParams . size > 0 ) {
445
+ url . searchParams . set (
446
+ "_routes" ,
447
+ matches
448
+ . filter ( ( m ) => routesParams . has ( m . route . id ) )
449
+ . map ( ( m ) => m . route . id )
450
+ . join ( "," )
451
+ ) ;
434
452
}
453
+
454
+ let data = await fetchAndDecode ( url , init ) ;
455
+ singleFetchDfd . resolve ( data . data as SingleFetchResults ) ;
435
456
} catch ( e ) {
436
- serverLoaderRouteDfds . forEach ( ( dfd , routeId ) => {
437
- results [ routeId ] = {
438
- type : "error" ,
439
- result : e ,
440
- } ;
441
- dfd . reject ( results [ routeId ] ) ;
442
- } ) ;
457
+ singleFetchDfd . reject ( e as Error ) ;
443
458
}
444
459
}
445
460
@@ -658,18 +673,17 @@ function unwrapSingleFetchResult(result: SingleFetchResult, routeId: string) {
658
673
}
659
674
}
660
675
661
- type Deferred = ReturnType < typeof createDeferred > ;
662
676
function createDeferred < T = unknown > ( ) {
663
677
let resolve : ( val ?: any ) => Promise < void > ;
664
- let reject : ( error ?: unknown ) => Promise < void > ;
678
+ let reject : ( error ?: Error ) => Promise < void > ;
665
679
let promise = new Promise < T > ( ( res , rej ) => {
666
680
resolve = async ( val : T ) => {
667
681
res ( val ) ;
668
682
try {
669
683
await promise ;
670
684
} catch ( e ) { }
671
685
} ;
672
- reject = async ( error ?: unknown ) => {
686
+ reject = async ( error ?: Error ) => {
673
687
rej ( error ) ;
674
688
try {
675
689
await promise ;
0 commit comments