@@ -894,33 +894,18 @@ export function createRouter(init: RouterInit): Router {
894894 // were marked for explicit hydration
895895 let loaderData = init . hydrationData ? init . hydrationData . loaderData : null ;
896896 let errors = init . hydrationData ? init . hydrationData . errors : null ;
897- let isRouteInitialized = ( m : AgnosticDataRouteMatch ) => {
898- // No loader, nothing to initialize
899- if ( ! m . route . loader ) {
900- return true ;
901- }
902- // Explicitly opting-in to running on hydration
903- if (
904- typeof m . route . loader === "function" &&
905- m . route . loader . hydrate === true
906- ) {
907- return false ;
908- }
909- // Otherwise, initialized if hydrated with data or an error
910- return (
911- ( loaderData && loaderData [ m . route . id ] !== undefined ) ||
912- ( errors && errors [ m . route . id ] !== undefined )
913- ) ;
914- } ;
915-
916897 // If errors exist, don't consider routes below the boundary
917898 if ( errors ) {
918899 let idx = initialMatches . findIndex (
919900 ( m ) => errors ! [ m . route . id ] !== undefined
920901 ) ;
921- initialized = initialMatches . slice ( 0 , idx + 1 ) . every ( isRouteInitialized ) ;
902+ initialized = initialMatches
903+ . slice ( 0 , idx + 1 )
904+ . every ( ( m ) => ! shouldLoadRouteOnHydration ( m . route , loaderData , errors ) ) ;
922905 } else {
923- initialized = initialMatches . every ( isRouteInitialized ) ;
906+ initialized = initialMatches . every (
907+ ( m ) => ! shouldLoadRouteOnHydration ( m . route , loaderData , errors )
908+ ) ;
924909 }
925910 } else {
926911 // Without partial hydration - we're initialized if we were provided any
@@ -1559,7 +1544,7 @@ export function createRouter(init: RouterInit): Router {
15591544 // Short circuit if it's only a hash change and not a revalidation or
15601545 // mutation submission.
15611546 //
1562- // Ignore on initial page loads because since the initial load will always
1547+ // Ignore on initial page loads because since the initial hydration will always
15631548 // be "same hash". For example, on /page#hash and submit a <Form method="post">
15641549 // which will default to a navigation to /page
15651550 if (
@@ -2073,13 +2058,9 @@ export function createRouter(init: RouterInit): Router {
20732058 } ) ;
20742059 } ) ;
20752060
2076- // During partial hydration, preserve SSR errors for routes that don't re-run
2061+ // Preserve SSR errors during partial hydration
20772062 if ( future . v7_partialHydration && initialHydration && state . errors ) {
2078- Object . entries ( state . errors )
2079- . filter ( ( [ id ] ) => ! matchesToLoad . some ( ( m ) => m . route . id === id ) )
2080- . forEach ( ( [ routeId , error ] ) => {
2081- errors = Object . assign ( errors || { } , { [ routeId ] : error } ) ;
2082- } ) ;
2063+ errors = { ...state . errors , ...errors } ;
20832064 }
20842065
20852066 let updatedFetchers = markFetchRedirectsDone ( ) ;
@@ -4352,20 +4333,18 @@ function normalizeNavigateOptions(
43524333 return { path : createPath ( parsedPath ) , submission } ;
43534334}
43544335
4355- // Filter out all routes below any caught error as they aren't going to
4336+ // Filter out all routes at/ below any caught error as they aren't going to
43564337// render so we don't need to load them
43574338function getLoaderMatchesUntilBoundary (
43584339 matches : AgnosticDataRouteMatch [ ] ,
4359- boundaryId : string
4340+ boundaryId : string ,
4341+ includeBoundary = false
43604342) {
4361- let boundaryMatches = matches ;
4362- if ( boundaryId ) {
4363- let index = matches . findIndex ( ( m ) => m . route . id === boundaryId ) ;
4364- if ( index >= 0 ) {
4365- boundaryMatches = matches . slice ( 0 , index ) ;
4366- }
4343+ let index = matches . findIndex ( ( m ) => m . route . id === boundaryId ) ;
4344+ if ( index >= 0 ) {
4345+ return matches . slice ( 0 , includeBoundary ? index + 1 : index ) ;
43674346 }
4368- return boundaryMatches ;
4347+ return matches ;
43694348}
43704349
43714350function getMatchesToLoad (
@@ -4374,7 +4353,7 @@ function getMatchesToLoad(
43744353 matches : AgnosticDataRouteMatch [ ] ,
43754354 submission : Submission | undefined ,
43764355 location : Location ,
4377- isInitialLoad : boolean ,
4356+ initialHydration : boolean ,
43784357 skipActionErrorRevalidation : boolean ,
43794358 isRevalidationRequired : boolean ,
43804359 cancelledDeferredRoutes : string [ ] ,
@@ -4395,13 +4374,26 @@ function getMatchesToLoad(
43954374 let nextUrl = history . createURL ( location ) ;
43964375
43974376 // Pick navigation matches that are net-new or qualify for revalidation
4398- let boundaryId =
4399- pendingActionResult && isErrorResult ( pendingActionResult [ 1 ] )
4400- ? pendingActionResult [ 0 ]
4401- : undefined ;
4402- let boundaryMatches = boundaryId
4403- ? getLoaderMatchesUntilBoundary ( matches , boundaryId )
4404- : matches ;
4377+ let boundaryMatches = matches ;
4378+ if ( initialHydration && state . errors ) {
4379+ // On initial hydration, only consider matches up to _and including_ the boundary.
4380+ // This is inclusive to handle cases where a server loader ran successfully,
4381+ // a child server loader bubbled up to this route, but this route has
4382+ // `clientLoader.hydrate` so we want to still run the `clientLoader` so that
4383+ // we have a complete version of `loaderData`
4384+ boundaryMatches = getLoaderMatchesUntilBoundary (
4385+ matches ,
4386+ Object . keys ( state . errors ) [ 0 ] ,
4387+ true
4388+ ) ;
4389+ } else if ( pendingActionResult && isErrorResult ( pendingActionResult [ 1 ] ) ) {
4390+ // If an action threw an error, we call loaders up to, but not including the
4391+ // boundary
4392+ boundaryMatches = getLoaderMatchesUntilBoundary (
4393+ matches ,
4394+ pendingActionResult [ 0 ]
4395+ ) ;
4396+ }
44054397
44064398 // Don't revalidate loaders by default after action 4xx/5xx responses
44074399 // when the flag is enabled. They can still opt-into revalidation via
@@ -4423,15 +4415,8 @@ function getMatchesToLoad(
44234415 return false ;
44244416 }
44254417
4426- if ( isInitialLoad ) {
4427- if ( typeof route . loader !== "function" || route . loader . hydrate ) {
4428- return true ;
4429- }
4430- return (
4431- state . loaderData [ route . id ] === undefined &&
4432- // Don't re-run if the loader ran and threw an error
4433- ( ! state . errors || state . errors [ route . id ] === undefined )
4434- ) ;
4418+ if ( initialHydration ) {
4419+ return shouldLoadRouteOnHydration ( route , state . loaderData , state . errors ) ;
44354420 }
44364421
44374422 // Always call the loader on new route instances and pending defer cancellations
@@ -4473,12 +4458,12 @@ function getMatchesToLoad(
44734458 let revalidatingFetchers : RevalidatingFetcher [ ] = [ ] ;
44744459 fetchLoadMatches . forEach ( ( f , key ) => {
44754460 // Don't revalidate:
4476- // - on initial load (shouldn't be any fetchers then anyway)
4461+ // - on initial hydration (shouldn't be any fetchers then anyway)
44774462 // - if fetcher won't be present in the subsequent render
44784463 // - no longer matches the URL (v7_fetcherPersist=false)
44794464 // - was unmounted but persisted due to v7_fetcherPersist=true
44804465 if (
4481- isInitialLoad ||
4466+ initialHydration ||
44824467 ! matches . some ( ( m ) => m . route . id === f . routeId ) ||
44834468 deletedFetchers . has ( key )
44844469 ) {
@@ -4558,6 +4543,38 @@ function getMatchesToLoad(
45584543 return [ navigationMatches , revalidatingFetchers ] ;
45594544}
45604545
4546+ function shouldLoadRouteOnHydration (
4547+ route : AgnosticDataRouteObject ,
4548+ loaderData : RouteData | null | undefined ,
4549+ errors : RouteData | null | undefined
4550+ ) {
4551+ // We dunno if we have a loader - gotta find out!
4552+ if ( route . lazy ) {
4553+ return true ;
4554+ }
4555+
4556+ // No loader, nothing to initialize
4557+ if ( ! route . loader ) {
4558+ return false ;
4559+ }
4560+
4561+ let hasData = loaderData != null && loaderData [ route . id ] !== undefined ;
4562+ let hasError = errors != null && errors [ route . id ] !== undefined ;
4563+
4564+ // Don't run if we error'd during SSR
4565+ if ( ! hasData && hasError ) {
4566+ return false ;
4567+ }
4568+
4569+ // Explicitly opting-in to running on hydration
4570+ if ( typeof route . loader === "function" && route . loader . hydrate === true ) {
4571+ return true ;
4572+ }
4573+
4574+ // Otherwise, run if we're not yet initialized with anything
4575+ return ! hasData && ! hasError ;
4576+ }
4577+
45614578function isNewLoader (
45624579 currentLoaderData : RouteData ,
45634580 currentMatch : AgnosticDataRouteMatch ,
0 commit comments