@remix-run/router 0.0.0-experimental-bcda00aaf → 0.0.0-experimental-3be88c6fb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "0.0.0-experimental-bcda00aaf",
3
+ "version": "0.0.0-experimental-3be88c6fb",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -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
@@ -1525,10 +1510,7 @@ export function createRouter(init: RouterInit): Router {
1525
1510
 
1526
1511
  let routesToUse = inFlightDataRoutes || dataRoutes;
1527
1512
  let loadingNavigation = opts && opts.overrideNavigation;
1528
- let matches =
1529
- isUninterruptedRevalidation && inFlightDataRoutes == null
1530
- ? state.matches
1531
- : matchRoutes(routesToUse, location, basename);
1513
+ let matches = matchRoutes(routesToUse, location, basename);
1532
1514
  let flushSync = (opts && opts.flushSync) === true;
1533
1515
 
1534
1516
  let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname);
@@ -1558,7 +1540,7 @@ export function createRouter(init: RouterInit): Router {
1558
1540
  // Short circuit if it's only a hash change and not a revalidation or
1559
1541
  // mutation submission.
1560
1542
  //
1561
- // 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
1562
1544
  // be "same hash". For example, on /page#hash and submit a <Form method="post">
1563
1545
  // which will default to a navigation to /page
1564
1546
  if (
@@ -1982,9 +1964,7 @@ export function createRouter(init: RouterInit): Router {
1982
1964
  }
1983
1965
 
1984
1966
  revalidatingFetchers.forEach((rf) => {
1985
- if (fetchControllers.has(rf.key)) {
1986
- abortFetcher(rf.key);
1987
- }
1967
+ abortFetcher(rf.key);
1988
1968
  if (rf.controller) {
1989
1969
  // Fetchers use an independent AbortController so that aborting a fetcher
1990
1970
  // (via deleteFetcher) does not abort the triggering navigation that
@@ -2025,6 +2005,7 @@ export function createRouter(init: RouterInit): Router {
2025
2005
  abortPendingFetchRevalidations
2026
2006
  );
2027
2007
  }
2008
+
2028
2009
  revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
2029
2010
 
2030
2011
  // If any loaders returned a redirect Response, start a new REPLACE navigation
@@ -2052,7 +2033,6 @@ export function createRouter(init: RouterInit): Router {
2052
2033
  let { loaderData, errors } = processLoaderData(
2053
2034
  state,
2054
2035
  matches,
2055
- matchesToLoad,
2056
2036
  loaderResults,
2057
2037
  pendingActionResult,
2058
2038
  revalidatingFetchers,
@@ -2072,13 +2052,9 @@ export function createRouter(init: RouterInit): Router {
2072
2052
  });
2073
2053
  });
2074
2054
 
2075
- // During partial hydration, preserve SSR errors for routes that don't re-run
2055
+ // Preserve SSR errors during partial hydration
2076
2056
  if (future.v7_partialHydration && initialHydration && state.errors) {
2077
- Object.entries(state.errors)
2078
- .filter(([id]) => !matchesToLoad.some((m) => m.route.id === id))
2079
- .forEach(([routeId, error]) => {
2080
- errors = Object.assign(errors || {}, { [routeId]: error });
2081
- });
2057
+ errors = { ...state.errors, ...errors };
2082
2058
  }
2083
2059
 
2084
2060
  let updatedFetchers = markFetchRedirectsDone();
@@ -2142,7 +2118,8 @@ export function createRouter(init: RouterInit): Router {
2142
2118
  );
2143
2119
  }
2144
2120
 
2145
- if (fetchControllers.has(key)) abortFetcher(key);
2121
+ abortFetcher(key);
2122
+
2146
2123
  let flushSync = (opts && opts.flushSync) === true;
2147
2124
 
2148
2125
  let routesToUse = inFlightDataRoutes || dataRoutes;
@@ -2187,7 +2164,7 @@ export function createRouter(init: RouterInit): Router {
2187
2164
 
2188
2165
  let match = getTargetMatch(matches, path);
2189
2166
 
2190
- pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2167
+ let preventScrollReset = (opts && opts.preventScrollReset) === true;
2191
2168
 
2192
2169
  if (submission && isMutationMethod(submission.formMethod)) {
2193
2170
  handleFetcherAction(
@@ -2198,6 +2175,7 @@ export function createRouter(init: RouterInit): Router {
2198
2175
  matches,
2199
2176
  fogOfWar.active,
2200
2177
  flushSync,
2178
+ preventScrollReset,
2201
2179
  submission
2202
2180
  );
2203
2181
  return;
@@ -2214,6 +2192,7 @@ export function createRouter(init: RouterInit): Router {
2214
2192
  matches,
2215
2193
  fogOfWar.active,
2216
2194
  flushSync,
2195
+ preventScrollReset,
2217
2196
  submission
2218
2197
  );
2219
2198
  }
@@ -2228,6 +2207,7 @@ export function createRouter(init: RouterInit): Router {
2228
2207
  requestMatches: AgnosticDataRouteMatch[],
2229
2208
  isFogOfWar: boolean,
2230
2209
  flushSync: boolean,
2210
+ preventScrollReset: boolean,
2231
2211
  submission: Submission
2232
2212
  ) {
2233
2213
  interruptActiveLoads();
@@ -2342,6 +2322,7 @@ export function createRouter(init: RouterInit): Router {
2342
2322
  updateFetcherState(key, getLoadingFetcher(submission));
2343
2323
  return startRedirectNavigation(fetchRequest, actionResult, false, {
2344
2324
  fetcherSubmission: submission,
2325
+ preventScrollReset,
2345
2326
  });
2346
2327
  }
2347
2328
  }
@@ -2411,9 +2392,7 @@ export function createRouter(init: RouterInit): Router {
2411
2392
  existingFetcher ? existingFetcher.data : undefined
2412
2393
  );
2413
2394
  state.fetchers.set(staleKey, revalidatingFetcher);
2414
- if (fetchControllers.has(staleKey)) {
2415
- abortFetcher(staleKey);
2416
- }
2395
+ abortFetcher(staleKey);
2417
2396
  if (rf.controller) {
2418
2397
  fetchControllers.set(staleKey, rf.controller);
2419
2398
  }
@@ -2456,7 +2435,8 @@ export function createRouter(init: RouterInit): Router {
2456
2435
  return startRedirectNavigation(
2457
2436
  revalidationRequest,
2458
2437
  redirect.result,
2459
- false
2438
+ false,
2439
+ { preventScrollReset }
2460
2440
  );
2461
2441
  }
2462
2442
 
@@ -2469,7 +2449,8 @@ export function createRouter(init: RouterInit): Router {
2469
2449
  return startRedirectNavigation(
2470
2450
  revalidationRequest,
2471
2451
  redirect.result,
2472
- false
2452
+ false,
2453
+ { preventScrollReset }
2473
2454
  );
2474
2455
  }
2475
2456
 
@@ -2477,7 +2458,6 @@ export function createRouter(init: RouterInit): Router {
2477
2458
  let { loaderData, errors } = processLoaderData(
2478
2459
  state,
2479
2460
  matches,
2480
- matchesToLoad,
2481
2461
  loaderResults,
2482
2462
  undefined,
2483
2463
  revalidatingFetchers,
@@ -2537,6 +2517,7 @@ export function createRouter(init: RouterInit): Router {
2537
2517
  matches: AgnosticDataRouteMatch[],
2538
2518
  isFogOfWar: boolean,
2539
2519
  flushSync: boolean,
2520
+ preventScrollReset: boolean,
2540
2521
  submission?: Submission
2541
2522
  ) {
2542
2523
  let existingFetcher = state.fetchers.get(key);
@@ -2633,7 +2614,9 @@ export function createRouter(init: RouterInit): Router {
2633
2614
  return;
2634
2615
  } else {
2635
2616
  fetchRedirectIds.add(key);
2636
- await startRedirectNavigation(fetchRequest, result, false);
2617
+ await startRedirectNavigation(fetchRequest, result, false, {
2618
+ preventScrollReset,
2619
+ });
2637
2620
  return;
2638
2621
  }
2639
2622
  }
@@ -2676,10 +2659,12 @@ export function createRouter(init: RouterInit): Router {
2676
2659
  {
2677
2660
  submission,
2678
2661
  fetcherSubmission,
2662
+ preventScrollReset,
2679
2663
  replace,
2680
2664
  }: {
2681
2665
  submission?: Submission;
2682
2666
  fetcherSubmission?: Submission;
2667
+ preventScrollReset?: boolean;
2683
2668
  replace?: boolean;
2684
2669
  } = {}
2685
2670
  ) {
@@ -2760,7 +2745,7 @@ export function createRouter(init: RouterInit): Router {
2760
2745
  formAction: location,
2761
2746
  },
2762
2747
  // Preserve these flags across redirects
2763
- preventScrollReset: pendingPreventScrollReset,
2748
+ preventScrollReset: preventScrollReset || pendingPreventScrollReset,
2764
2749
  enableViewTransition: isNavigation
2765
2750
  ? pendingViewTransitionEnabled
2766
2751
  : undefined,
@@ -2777,7 +2762,7 @@ export function createRouter(init: RouterInit): Router {
2777
2762
  // Send fetcher submissions through for shouldRevalidate
2778
2763
  fetcherSubmission,
2779
2764
  // Preserve these flags across redirects
2780
- preventScrollReset: pendingPreventScrollReset,
2765
+ preventScrollReset: preventScrollReset || pendingPreventScrollReset,
2781
2766
  enableViewTransition: isNavigation
2782
2767
  ? pendingViewTransitionEnabled
2783
2768
  : undefined,
@@ -2926,8 +2911,8 @@ export function createRouter(init: RouterInit): Router {
2926
2911
  fetchLoadMatches.forEach((_, key) => {
2927
2912
  if (fetchControllers.has(key)) {
2928
2913
  cancelledFetcherLoads.add(key);
2929
- abortFetcher(key);
2930
2914
  }
2915
+ abortFetcher(key);
2931
2916
  });
2932
2917
  }
2933
2918
 
@@ -3010,9 +2995,10 @@ export function createRouter(init: RouterInit): Router {
3010
2995
 
3011
2996
  function abortFetcher(key: string) {
3012
2997
  let controller = fetchControllers.get(key);
3013
- invariant(controller, `Expected fetch controller: ${key}`);
3014
- controller.abort();
3015
- fetchControllers.delete(key);
2998
+ if (controller) {
2999
+ controller.abort();
3000
+ fetchControllers.delete(key);
3001
+ }
3016
3002
  }
3017
3003
 
3018
3004
  function markFetchersDone(keys: string[]) {
@@ -3290,21 +3276,30 @@ export function createRouter(init: RouterInit): Router {
3290
3276
  pathname: string,
3291
3277
  signal: AbortSignal
3292
3278
  ): Promise<DiscoverRoutesResult> {
3279
+ if (!patchRoutesOnNavigationImpl) {
3280
+ return { type: "success", matches };
3281
+ }
3282
+
3293
3283
  let partialMatches: AgnosticDataRouteMatch[] | null = matches;
3294
3284
  while (true) {
3295
3285
  let isNonHMR = inFlightDataRoutes == null;
3296
3286
  let routesToUse = inFlightDataRoutes || dataRoutes;
3287
+ let localManifest = manifest;
3297
3288
  try {
3298
- await loadLazyRouteChildren(
3299
- patchRoutesOnNavigationImpl!,
3300
- pathname,
3301
- partialMatches,
3302
- routesToUse,
3303
- manifest,
3304
- mapRouteProperties,
3305
- pendingPatchRoutes,
3306
- signal
3307
- );
3289
+ await patchRoutesOnNavigationImpl({
3290
+ path: pathname,
3291
+ matches: partialMatches,
3292
+ patch: (routeId, children) => {
3293
+ if (signal.aborted) return;
3294
+ patchRoutesImpl(
3295
+ routeId,
3296
+ children,
3297
+ routesToUse,
3298
+ localManifest,
3299
+ mapRouteProperties
3300
+ );
3301
+ },
3302
+ });
3308
3303
  } catch (e) {
3309
3304
  return { type: "error", error: e, partialMatches };
3310
3305
  } finally {
@@ -3314,7 +3309,7 @@ export function createRouter(init: RouterInit): Router {
3314
3309
  // trigger a re-run of memoized `router.routes` dependencies.
3315
3310
  // HMR will already update the identity and reflow when it lands
3316
3311
  // `inFlightDataRoutes` in `completeNavigation`
3317
- if (isNonHMR) {
3312
+ if (isNonHMR && !signal.aborted) {
3318
3313
  dataRoutes = [...dataRoutes];
3319
3314
  }
3320
3315
  }
@@ -4161,16 +4156,23 @@ function normalizeTo(
4161
4156
  path.hash = location.hash;
4162
4157
  }
4163
4158
 
4164
- // Add an ?index param for matched index routes if we don't already have one
4165
- if (
4166
- (to == null || to === "" || to === ".") &&
4167
- activeRouteMatch &&
4168
- activeRouteMatch.route.index &&
4169
- !hasNakedIndexQuery(path.search)
4170
- ) {
4171
- path.search = path.search
4172
- ? path.search.replace(/^\?/, "?index&")
4173
- : "?index";
4159
+ // Account for `?index` params when routing to the current location
4160
+ if ((to == null || to === "" || to === ".") && activeRouteMatch) {
4161
+ let nakedIndex = hasNakedIndexQuery(path.search);
4162
+ if (activeRouteMatch.route.index && !nakedIndex) {
4163
+ // Add one when we're targeting an index route
4164
+ path.search = path.search
4165
+ ? path.search.replace(/^\?/, "?index&")
4166
+ : "?index";
4167
+ } else if (!activeRouteMatch.route.index && nakedIndex) {
4168
+ // Remove existing ones when we're not
4169
+ let params = new URLSearchParams(path.search);
4170
+ let indexValues = params.getAll("index");
4171
+ params.delete("index");
4172
+ indexValues.filter((v) => v).forEach((v) => params.append("index", v));
4173
+ let qs = params.toString();
4174
+ path.search = qs ? `?${qs}` : "";
4175
+ }
4174
4176
  }
4175
4177
 
4176
4178
  // If we're operating within a basename, prepend it to the pathname. If
@@ -4334,20 +4336,18 @@ function normalizeNavigateOptions(
4334
4336
  return { path: createPath(parsedPath), submission };
4335
4337
  }
4336
4338
 
4337
- // 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
4338
4340
  // render so we don't need to load them
4339
4341
  function getLoaderMatchesUntilBoundary(
4340
4342
  matches: AgnosticDataRouteMatch[],
4341
- boundaryId: string
4343
+ boundaryId: string,
4344
+ includeBoundary = false
4342
4345
  ) {
4343
- let boundaryMatches = matches;
4344
- if (boundaryId) {
4345
- let index = matches.findIndex((m) => m.route.id === boundaryId);
4346
- if (index >= 0) {
4347
- boundaryMatches = matches.slice(0, index);
4348
- }
4346
+ let index = matches.findIndex((m) => m.route.id === boundaryId);
4347
+ if (index >= 0) {
4348
+ return matches.slice(0, includeBoundary ? index + 1 : index);
4349
4349
  }
4350
- return boundaryMatches;
4350
+ return matches;
4351
4351
  }
4352
4352
 
4353
4353
  function getMatchesToLoad(
@@ -4356,7 +4356,7 @@ function getMatchesToLoad(
4356
4356
  matches: AgnosticDataRouteMatch[],
4357
4357
  submission: Submission | undefined,
4358
4358
  location: Location,
4359
- isInitialLoad: boolean,
4359
+ initialHydration: boolean,
4360
4360
  skipActionErrorRevalidation: boolean,
4361
4361
  isRevalidationRequired: boolean,
4362
4362
  cancelledDeferredRoutes: string[],
@@ -4377,13 +4377,26 @@ function getMatchesToLoad(
4377
4377
  let nextUrl = history.createURL(location);
4378
4378
 
4379
4379
  // Pick navigation matches that are net-new or qualify for revalidation
4380
- let boundaryId =
4381
- pendingActionResult && isErrorResult(pendingActionResult[1])
4382
- ? pendingActionResult[0]
4383
- : undefined;
4384
- let boundaryMatches = boundaryId
4385
- ? getLoaderMatchesUntilBoundary(matches, boundaryId)
4386
- : 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
+ }
4387
4400
 
4388
4401
  // Don't revalidate loaders by default after action 4xx/5xx responses
4389
4402
  // when the flag is enabled. They can still opt-into revalidation via
@@ -4405,15 +4418,8 @@ function getMatchesToLoad(
4405
4418
  return false;
4406
4419
  }
4407
4420
 
4408
- if (isInitialLoad) {
4409
- if (typeof route.loader !== "function" || route.loader.hydrate) {
4410
- return true;
4411
- }
4412
- return (
4413
- state.loaderData[route.id] === undefined &&
4414
- // Don't re-run if the loader ran and threw an error
4415
- (!state.errors || state.errors[route.id] === undefined)
4416
- );
4421
+ if (initialHydration) {
4422
+ return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
4417
4423
  }
4418
4424
 
4419
4425
  // Always call the loader on new route instances and pending defer cancellations
@@ -4455,12 +4461,12 @@ function getMatchesToLoad(
4455
4461
  let revalidatingFetchers: RevalidatingFetcher[] = [];
4456
4462
  fetchLoadMatches.forEach((f, key) => {
4457
4463
  // Don't revalidate:
4458
- // - on initial load (shouldn't be any fetchers then anyway)
4464
+ // - on initial hydration (shouldn't be any fetchers then anyway)
4459
4465
  // - if fetcher won't be present in the subsequent render
4460
4466
  // - no longer matches the URL (v7_fetcherPersist=false)
4461
4467
  // - was unmounted but persisted due to v7_fetcherPersist=true
4462
4468
  if (
4463
- isInitialLoad ||
4469
+ initialHydration ||
4464
4470
  !matches.some((m) => m.route.id === f.routeId) ||
4465
4471
  deletedFetchers.has(key)
4466
4472
  ) {
@@ -4540,6 +4546,38 @@ function getMatchesToLoad(
4540
4546
  return [navigationMatches, revalidatingFetchers];
4541
4547
  }
4542
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
+
4543
4581
  function isNewLoader(
4544
4582
  currentLoaderData: RouteData,
4545
4583
  currentMatch: AgnosticDataRouteMatch,
@@ -4589,53 +4627,6 @@ function shouldRevalidateLoader(
4589
4627
  return arg.defaultShouldRevalidate;
4590
4628
  }
4591
4629
 
4592
- /**
4593
- * Idempotent utility to execute patchRoutesOnNavigation() to lazily load route
4594
- * definitions and update the routes/routeManifest
4595
- */
4596
- async function loadLazyRouteChildren(
4597
- patchRoutesOnNavigationImpl: AgnosticPatchRoutesOnNavigationFunction,
4598
- path: string,
4599
- matches: AgnosticDataRouteMatch[],
4600
- routes: AgnosticDataRouteObject[],
4601
- manifest: RouteManifest,
4602
- mapRouteProperties: MapRoutePropertiesFunction,
4603
- pendingRouteChildren: Map<
4604
- string,
4605
- ReturnType<typeof patchRoutesOnNavigationImpl>
4606
- >,
4607
- signal: AbortSignal
4608
- ) {
4609
- let key = [path, ...matches.map((m) => m.route.id)].join("-");
4610
- try {
4611
- let pending = pendingRouteChildren.get(key);
4612
- if (!pending) {
4613
- pending = patchRoutesOnNavigationImpl({
4614
- path,
4615
- matches,
4616
- patch: (routeId, children) => {
4617
- if (!signal.aborted) {
4618
- patchRoutesImpl(
4619
- routeId,
4620
- children,
4621
- routes,
4622
- manifest,
4623
- mapRouteProperties
4624
- );
4625
- }
4626
- },
4627
- });
4628
- pendingRouteChildren.set(key, pending);
4629
- }
4630
-
4631
- if (pending && isPromise<AgnosticRouteObject[]>(pending)) {
4632
- await pending;
4633
- }
4634
- } finally {
4635
- pendingRouteChildren.delete(key);
4636
- }
4637
- }
4638
-
4639
4630
  function patchRoutesImpl(
4640
4631
  routeId: string | null,
4641
4632
  children: AgnosticRouteObject[],
@@ -4662,12 +4653,9 @@ function patchRoutesImpl(
4662
4653
  // to simplify user-land code. This is useful because we re-call the
4663
4654
  // `patchRoutesOnNavigation` function for matched routes with params.
4664
4655
  let uniqueChildren = children.filter(
4665
- (a) =>
4666
- !childrenToPatch.some(
4667
- (b) =>
4668
- a.index === b.index &&
4669
- a.path === b.path &&
4670
- a.caseSensitive === b.caseSensitive
4656
+ (newRoute) =>
4657
+ !childrenToPatch.some((existingRoute) =>
4658
+ isSameRoute(newRoute, existingRoute)
4671
4659
  )
4672
4660
  );
4673
4661
 
@@ -4681,6 +4669,46 @@ function patchRoutesImpl(
4681
4669
  childrenToPatch.push(...newRoutes);
4682
4670
  }
4683
4671
 
4672
+ function isSameRoute(
4673
+ newRoute: AgnosticRouteObject,
4674
+ existingRoute: AgnosticRouteObject
4675
+ ): boolean {
4676
+ // Most optimal check is by id
4677
+ if (
4678
+ "id" in newRoute &&
4679
+ "id" in existingRoute &&
4680
+ newRoute.id === existingRoute.id
4681
+ ) {
4682
+ return true;
4683
+ }
4684
+
4685
+ // Second is by pathing differences
4686
+ if (
4687
+ !(
4688
+ newRoute.index === existingRoute.index &&
4689
+ newRoute.path === existingRoute.path &&
4690
+ newRoute.caseSensitive === existingRoute.caseSensitive
4691
+ )
4692
+ ) {
4693
+ return false;
4694
+ }
4695
+
4696
+ // Pathless layout routes are trickier since we need to check children.
4697
+ // If they have no children then they're the same as far as we can tell
4698
+ if (
4699
+ (!newRoute.children || newRoute.children.length === 0) &&
4700
+ (!existingRoute.children || existingRoute.children.length === 0)
4701
+ ) {
4702
+ return true;
4703
+ }
4704
+
4705
+ // Otherwise, we look to see if every child in the new route is already
4706
+ // represented in the existing route's children
4707
+ return newRoute.children!.every((aChild, i) =>
4708
+ existingRoute.children?.some((bChild) => isSameRoute(aChild, bChild))
4709
+ );
4710
+ }
4711
+
4684
4712
  /**
4685
4713
  * Execute route.lazy() methods to lazily load route modules (loader, action,
4686
4714
  * shouldRevalidate) and update the routeManifest in place which shares objects
@@ -5302,7 +5330,6 @@ function processRouteLoaderData(
5302
5330
  function processLoaderData(
5303
5331
  state: RouterState,
5304
5332
  matches: AgnosticDataRouteMatch[],
5305
- matchesToLoad: AgnosticDataRouteMatch[],
5306
5333
  results: Record<string, DataResult>,
5307
5334
  pendingActionResult: PendingActionResult | undefined,
5308
5335
  revalidatingFetchers: RevalidatingFetcher[],