@@ -890,33 +890,18 @@ export function createRouter(init: RouterInit): Router {
890
890
// were marked for explicit hydration
891
891
let loaderData = init . hydrationData ? init . hydrationData . loaderData : null ;
892
892
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
-
912
893
// If errors exist, don't consider routes below the boundary
913
894
if ( errors ) {
914
895
let idx = initialMatches . findIndex (
915
896
( m ) => errors ! [ m . route . id ] !== undefined
916
897
) ;
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 ) ) ;
918
901
} else {
919
- initialized = initialMatches . every ( isRouteInitialized ) ;
902
+ initialized = initialMatches . every (
903
+ ( m ) => ! shouldLoadRouteOnHydration ( m . route , loaderData , errors )
904
+ ) ;
920
905
}
921
906
} else {
922
907
// Without partial hydration - we're initialized if we were provided any
@@ -1555,7 +1540,7 @@ export function createRouter(init: RouterInit): Router {
1555
1540
// Short circuit if it's only a hash change and not a revalidation or
1556
1541
// mutation submission.
1557
1542
//
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
1559
1544
// be "same hash". For example, on /page#hash and submit a <Form method="post">
1560
1545
// which will default to a navigation to /page
1561
1546
if (
@@ -2067,13 +2052,9 @@ export function createRouter(init: RouterInit): Router {
2067
2052
} ) ;
2068
2053
} ) ;
2069
2054
2070
- // During partial hydration, preserve SSR errors for routes that don't re-run
2055
+ // Preserve SSR errors during partial hydration
2071
2056
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 } ;
2077
2058
}
2078
2059
2079
2060
let updatedFetchers = markFetchRedirectsDone ( ) ;
@@ -4355,20 +4336,18 @@ function normalizeNavigateOptions(
4355
4336
return { path : createPath ( parsedPath ) , submission } ;
4356
4337
}
4357
4338
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
4359
4340
// render so we don't need to load them
4360
4341
function getLoaderMatchesUntilBoundary (
4361
4342
matches : AgnosticDataRouteMatch [ ] ,
4362
- boundaryId : string
4343
+ boundaryId : string ,
4344
+ includeBoundary = false
4363
4345
) {
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 ) ;
4370
4349
}
4371
- return boundaryMatches ;
4350
+ return matches ;
4372
4351
}
4373
4352
4374
4353
function getMatchesToLoad (
@@ -4377,7 +4356,7 @@ function getMatchesToLoad(
4377
4356
matches : AgnosticDataRouteMatch [ ] ,
4378
4357
submission : Submission | undefined ,
4379
4358
location : Location ,
4380
- isInitialLoad : boolean ,
4359
+ initialHydration : boolean ,
4381
4360
skipActionErrorRevalidation : boolean ,
4382
4361
isRevalidationRequired : boolean ,
4383
4362
cancelledDeferredRoutes : string [ ] ,
@@ -4398,13 +4377,26 @@ function getMatchesToLoad(
4398
4377
let nextUrl = history . createURL ( location ) ;
4399
4378
4400
4379
// 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
+ }
4408
4400
4409
4401
// Don't revalidate loaders by default after action 4xx/5xx responses
4410
4402
// when the flag is enabled. They can still opt-into revalidation via
@@ -4426,15 +4418,8 @@ function getMatchesToLoad(
4426
4418
return false ;
4427
4419
}
4428
4420
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 ) ;
4438
4423
}
4439
4424
4440
4425
// Always call the loader on new route instances and pending defer cancellations
@@ -4476,12 +4461,12 @@ function getMatchesToLoad(
4476
4461
let revalidatingFetchers : RevalidatingFetcher [ ] = [ ] ;
4477
4462
fetchLoadMatches . forEach ( ( f , key ) => {
4478
4463
// 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)
4480
4465
// - if fetcher won't be present in the subsequent render
4481
4466
// - no longer matches the URL (v7_fetcherPersist=false)
4482
4467
// - was unmounted but persisted due to v7_fetcherPersist=true
4483
4468
if (
4484
- isInitialLoad ||
4469
+ initialHydration ||
4485
4470
! matches . some ( ( m ) => m . route . id === f . routeId ) ||
4486
4471
deletedFetchers . has ( key )
4487
4472
) {
@@ -4561,6 +4546,38 @@ function getMatchesToLoad(
4561
4546
return [ navigationMatches , revalidatingFetchers ] ;
4562
4547
}
4563
4548
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
+
4564
4581
function isNewLoader (
4565
4582
currentLoaderData : RouteData ,
4566
4583
currentMatch : AgnosticDataRouteMatch ,
0 commit comments