@@ -890,33 +890,18 @@ export function createRouter(init: RouterInit): Router {
890890 // were marked for explicit hydration
891891 let loaderData = init . hydrationData ? init . hydrationData . loaderData : null ;
892892 let errors = init . hydrationData ? init . hydrationData . errors : null ;
893- let isRouteInitialized = ( m : AgnosticDataRouteMatch ) => {
894- // No loader, nothing to initialize
895- if ( ! m . route . loader ) {
896- return true ;
897- }
898- // Explicitly opting-in to running on hydration
899- if (
900- typeof m . route . loader === "function" &&
901- m . route . loader . hydrate === true
902- ) {
903- return false ;
904- }
905- // Otherwise, initialized if hydrated with data or an error
906- return (
907- ( loaderData && loaderData [ m . route . id ] !== undefined ) ||
908- ( errors && errors [ m . route . id ] !== undefined )
909- ) ;
910- } ;
911-
912893 // If errors exist, don't consider routes below the boundary
913894 if ( errors ) {
914895 let idx = initialMatches . findIndex (
915896 ( m ) => errors ! [ m . route . id ] !== undefined
916897 ) ;
917- initialized = initialMatches . slice ( 0 , idx + 1 ) . every ( isRouteInitialized ) ;
898+ initialized = initialMatches
899+ . slice ( 0 , idx + 1 )
900+ . every ( ( m ) => ! shouldLoadRouteOnHydration ( m . route , loaderData , errors ) ) ;
918901 } else {
919- initialized = initialMatches . every ( isRouteInitialized ) ;
902+ initialized = initialMatches . every (
903+ ( m ) => ! shouldLoadRouteOnHydration ( m . route , loaderData , errors )
904+ ) ;
920905 }
921906 } else {
922907 // Without partial hydration - we're initialized if we were provided any
@@ -1555,7 +1540,7 @@ export function createRouter(init: RouterInit): Router {
15551540 // Short circuit if it's only a hash change and not a revalidation or
15561541 // mutation submission.
15571542 //
1558- // Ignore on initial page loads because since the initial load will always
1543+ // Ignore on initial page loads because since the initial hydration will always
15591544 // be "same hash". For example, on /page#hash and submit a <Form method="post">
15601545 // which will default to a navigation to /page
15611546 if (
@@ -2067,13 +2052,9 @@ export function createRouter(init: RouterInit): Router {
20672052 } ) ;
20682053 } ) ;
20692054
2070- // During partial hydration, preserve SSR errors for routes that don't re-run
2055+ // Preserve SSR errors during partial hydration
20712056 if ( future . v7_partialHydration && initialHydration && state . errors ) {
2072- Object . entries ( state . errors )
2073- . filter ( ( [ id ] ) => ! matchesToLoad . some ( ( m ) => m . route . id === id ) )
2074- . forEach ( ( [ routeId , error ] ) => {
2075- errors = Object . assign ( errors || { } , { [ routeId ] : error } ) ;
2076- } ) ;
2057+ errors = { ...state . errors , ...errors } ;
20772058 }
20782059
20792060 let updatedFetchers = markFetchRedirectsDone ( ) ;
@@ -4355,20 +4336,18 @@ function normalizeNavigateOptions(
43554336 return { path : createPath ( parsedPath ) , submission } ;
43564337}
43574338
4358- // Filter out all routes below any caught error as they aren't going to
4339+ // Filter out all routes at/ below any caught error as they aren't going to
43594340// render so we don't need to load them
43604341function getLoaderMatchesUntilBoundary (
43614342 matches : AgnosticDataRouteMatch [ ] ,
4362- boundaryId : string
4343+ boundaryId : string ,
4344+ includeBoundary = false
43634345) {
4364- let boundaryMatches = matches ;
4365- if ( boundaryId ) {
4366- let index = matches . findIndex ( ( m ) => m . route . id === boundaryId ) ;
4367- if ( index >= 0 ) {
4368- boundaryMatches = matches . slice ( 0 , index ) ;
4369- }
4346+ let index = matches . findIndex ( ( m ) => m . route . id === boundaryId ) ;
4347+ if ( index >= 0 ) {
4348+ return matches . slice ( 0 , includeBoundary ? index + 1 : index ) ;
43704349 }
4371- return boundaryMatches ;
4350+ return matches ;
43724351}
43734352
43744353function getMatchesToLoad (
@@ -4377,7 +4356,7 @@ function getMatchesToLoad(
43774356 matches : AgnosticDataRouteMatch [ ] ,
43784357 submission : Submission | undefined ,
43794358 location : Location ,
4380- isInitialLoad : boolean ,
4359+ initialHydration : boolean ,
43814360 skipActionErrorRevalidation : boolean ,
43824361 isRevalidationRequired : boolean ,
43834362 cancelledDeferredRoutes : string [ ] ,
@@ -4398,13 +4377,26 @@ function getMatchesToLoad(
43984377 let nextUrl = history . createURL ( location ) ;
43994378
44004379 // Pick navigation matches that are net-new or qualify for revalidation
4401- let boundaryId =
4402- pendingActionResult && isErrorResult ( pendingActionResult [ 1 ] )
4403- ? pendingActionResult [ 0 ]
4404- : undefined ;
4405- let boundaryMatches = boundaryId
4406- ? getLoaderMatchesUntilBoundary ( matches , boundaryId )
4407- : matches ;
4380+ let boundaryMatches = matches ;
4381+ if ( initialHydration && state . errors ) {
4382+ // On initial hydration, only consider matches up to _and including_ the boundary.
4383+ // This is inclusive to handle cases where a server loader ran successfully,
4384+ // a child server loader bubbled up to this route, but this route has
4385+ // `clientLoader.hydrate` so we want to still run the `clientLoader` so that
4386+ // we have a complete version of `loaderData`
4387+ boundaryMatches = getLoaderMatchesUntilBoundary (
4388+ matches ,
4389+ Object . keys ( state . errors ) [ 0 ] ,
4390+ true
4391+ ) ;
4392+ } else if ( pendingActionResult && isErrorResult ( pendingActionResult [ 1 ] ) ) {
4393+ // If an action threw an error, we call loaders up to, but not including the
4394+ // boundary
4395+ boundaryMatches = getLoaderMatchesUntilBoundary (
4396+ matches ,
4397+ pendingActionResult [ 0 ]
4398+ ) ;
4399+ }
44084400
44094401 // Don't revalidate loaders by default after action 4xx/5xx responses
44104402 // when the flag is enabled. They can still opt-into revalidation via
@@ -4426,15 +4418,8 @@ function getMatchesToLoad(
44264418 return false ;
44274419 }
44284420
4429- if ( isInitialLoad ) {
4430- if ( typeof route . loader !== "function" || route . loader . hydrate ) {
4431- return true ;
4432- }
4433- return (
4434- state . loaderData [ route . id ] === undefined &&
4435- // Don't re-run if the loader ran and threw an error
4436- ( ! state . errors || state . errors [ route . id ] === undefined )
4437- ) ;
4421+ if ( initialHydration ) {
4422+ return shouldLoadRouteOnHydration ( route , state . loaderData , state . errors ) ;
44384423 }
44394424
44404425 // Always call the loader on new route instances and pending defer cancellations
@@ -4476,12 +4461,12 @@ function getMatchesToLoad(
44764461 let revalidatingFetchers : RevalidatingFetcher [ ] = [ ] ;
44774462 fetchLoadMatches . forEach ( ( f , key ) => {
44784463 // Don't revalidate:
4479- // - on initial load (shouldn't be any fetchers then anyway)
4464+ // - on initial hydration (shouldn't be any fetchers then anyway)
44804465 // - if fetcher won't be present in the subsequent render
44814466 // - no longer matches the URL (v7_fetcherPersist=false)
44824467 // - was unmounted but persisted due to v7_fetcherPersist=true
44834468 if (
4484- isInitialLoad ||
4469+ initialHydration ||
44854470 ! matches . some ( ( m ) => m . route . id === f . routeId ) ||
44864471 deletedFetchers . has ( key )
44874472 ) {
@@ -4561,6 +4546,38 @@ function getMatchesToLoad(
45614546 return [ navigationMatches , revalidatingFetchers ] ;
45624547}
45634548
4549+ function shouldLoadRouteOnHydration (
4550+ route : AgnosticDataRouteObject ,
4551+ loaderData : RouteData | null | undefined ,
4552+ errors : RouteData | null | undefined
4553+ ) {
4554+ // We dunno if we have a loader - gotta find out!
4555+ if ( route . lazy ) {
4556+ return true ;
4557+ }
4558+
4559+ // No loader, nothing to initialize
4560+ if ( ! route . loader ) {
4561+ return false ;
4562+ }
4563+
4564+ let hasData = loaderData != null && loaderData [ route . id ] !== undefined ;
4565+ let hasError = errors != null && errors [ route . id ] !== undefined ;
4566+
4567+ // Don't run if we error'd during SSR
4568+ if ( ! hasData && hasError ) {
4569+ return false ;
4570+ }
4571+
4572+ // Explicitly opting-in to running on hydration
4573+ if ( typeof route . loader === "function" && route . loader . hydrate === true ) {
4574+ return true ;
4575+ }
4576+
4577+ // Otherwise, run if we're not yet initialized with anything
4578+ return ! hasData && ! hasError ;
4579+ }
4580+
45644581function isNewLoader (
45654582 currentLoaderData : RouteData ,
45664583 currentMatch : AgnosticDataRouteMatch ,
0 commit comments