@@ -316,130 +316,145 @@ async function singleFetchLoaderNavigationStrategy(
316316 matches : DataStrategyFunctionArgs [ "matches" ] ,
317317 basename : string | undefined
318318) {
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 > ( ) ;
321322
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`
323325 let foundOptOutRoute = false ;
324326
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
326330 let routeDfds = matches . map ( ( ) => createDeferred < void > ( ) ) ;
331+ let routesLoadedPromise = Promise . all ( routeDfds . map ( ( d ) => d . promise ) ) ;
327332
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 > ( ) ;
330336
331- // Base URL/ RequestInit for calls to the server
337+ // Base URL and RequestInit for calls to the server
332338 let url = stripIndexParam ( singleFetchUrl ( request . url , basename ) ) ;
333339 let init = await createRequestInit ( request ) ;
334340
341+ // We'll build up this results object as we loop through matches
342+ let results : Record < string , DataStrategyResult > = { } ;
343+
335344 let resolvePromise = Promise . all (
336345 matches . map ( async ( m , i ) =>
337346 m . resolve ( async ( handler ) => {
338347 routeDfds [ i ] . resolve ( ) ;
339348
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+ }
360361
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 ;
371376 }
372- return ;
373377 }
374378
375379 // When a route has a client loader, it opts out of the singular call and
376380 // 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 ) {
379383 foundOptOutRoute = true ;
380384 }
381385 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 } ;
384393 } catch ( e ) {
385- results [ routeId ] = { type : "error" , result : e } ;
394+ results [ m . route . id ] = { type : "error" , result : e } ;
386395 }
387396 return ;
388397 }
389398
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+ }
393420 } )
394421 )
395422 ) ;
396423
397424 // 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 ;
399426
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
404430 //
405431 // One exception - if we are performing an HDR revalidation we have to call
406432 // the server in case a new loader has shown up that the manifest doesn't yet
407433 // know about
408434 if (
409- ( serverLoaderRouteDfds . size > 0 && router . state . initialized ) ||
410- window . __reactRouterHdrActive
435+ ( ! router . state . initialized || routesParams . size === 0 ) &&
436+ ! window . __reactRouterHdrActive
411437 ) {
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 {
419440 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+ ) ;
434452 }
453+
454+ let data = await fetchAndDecode ( url , init ) ;
455+ singleFetchDfd . resolve ( data . data as SingleFetchResults ) ;
435456 } 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 ) ;
443458 }
444459 }
445460
@@ -658,18 +673,17 @@ function unwrapSingleFetchResult(result: SingleFetchResult, routeId: string) {
658673 }
659674}
660675
661- type Deferred = ReturnType < typeof createDeferred > ;
662676function createDeferred < T = unknown > ( ) {
663677 let resolve : ( val ?: any ) => Promise < void > ;
664- let reject : ( error ?: unknown ) => Promise < void > ;
678+ let reject : ( error ?: Error ) => Promise < void > ;
665679 let promise = new Promise < T > ( ( res , rej ) => {
666680 resolve = async ( val : T ) => {
667681 res ( val ) ;
668682 try {
669683 await promise ;
670684 } catch ( e ) { }
671685 } ;
672- reject = async ( error ?: unknown ) => {
686+ reject = async ( error ?: Error ) => {
673687 rej ( error ) ;
674688 try {
675689 await promise ;
0 commit comments