@remix-run/router 1.19.2 → 1.20.0-pre.1

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/router.ts CHANGED
@@ -391,8 +391,8 @@ export interface RouterInit {
391
391
  future?: Partial<FutureConfig>;
392
392
  hydrationData?: HydrationState;
393
393
  window?: Window;
394
- unstable_patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction;
395
- unstable_dataStrategy?: DataStrategyFunction;
394
+ dataStrategy?: DataStrategyFunction;
395
+ patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction;
396
396
  }
397
397
 
398
398
  /**
@@ -422,7 +422,7 @@ export interface StaticHandler {
422
422
  opts?: {
423
423
  requestContext?: unknown;
424
424
  skipLoaderErrorBubbling?: boolean;
425
- unstable_dataStrategy?: DataStrategyFunction;
425
+ dataStrategy?: DataStrategyFunction;
426
426
  }
427
427
  ): Promise<StaticHandlerContext | Response>;
428
428
  queryRoute(
@@ -430,7 +430,7 @@ export interface StaticHandler {
430
430
  opts?: {
431
431
  routeId?: string;
432
432
  requestContext?: unknown;
433
- unstable_dataStrategy?: DataStrategyFunction;
433
+ dataStrategy?: DataStrategyFunction;
434
434
  }
435
435
  ): Promise<any>;
436
436
  }
@@ -448,8 +448,8 @@ export interface RouterSubscriber {
448
448
  state: RouterState,
449
449
  opts: {
450
450
  deletedFetchers: string[];
451
- unstable_viewTransitionOpts?: ViewTransitionOpts;
452
- unstable_flushSync: boolean;
451
+ viewTransitionOpts?: ViewTransitionOpts;
452
+ flushSync: boolean;
453
453
  }
454
454
  ): void;
455
455
  }
@@ -475,7 +475,7 @@ export type RelativeRoutingType = "route" | "path";
475
475
  type BaseNavigateOrFetchOptions = {
476
476
  preventScrollReset?: boolean;
477
477
  relative?: RelativeRoutingType;
478
- unstable_flushSync?: boolean;
478
+ flushSync?: boolean;
479
479
  };
480
480
 
481
481
  // Only allowed for navigations
@@ -483,7 +483,7 @@ type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
483
483
  replace?: boolean;
484
484
  state?: any;
485
485
  fromRouteId?: string;
486
- unstable_viewTransition?: boolean;
486
+ viewTransition?: boolean;
487
487
  };
488
488
 
489
489
  // Only allowed for submission navigations
@@ -797,8 +797,8 @@ export function createRouter(init: RouterInit): Router {
797
797
  );
798
798
  let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
799
799
  let basename = init.basename || "/";
800
- let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
801
- let patchRoutesOnNavigationImpl = init.unstable_patchRoutesOnNavigation;
800
+ let dataStrategyImpl = init.dataStrategy || defaultDataStrategy;
801
+ let patchRoutesOnNavigationImpl = init.patchRoutesOnNavigation;
802
802
 
803
803
  // Config driven behavior flags
804
804
  let future: FutureConfig = {
@@ -814,10 +814,6 @@ export function createRouter(init: RouterInit): Router {
814
814
  let unlistenHistory: (() => void) | null = null;
815
815
  // Externally-provided functions to call on all state changes
816
816
  let subscribers = new Set<RouterSubscriber>();
817
- // FIFO queue of previously discovered routes to prevent re-calling on
818
- // subsequent navigations to the same path
819
- let discoveredRoutesMaxSize = 1000;
820
- let discoveredRoutes = new Set<string>();
821
817
  // Externally-provided object to hold scroll restoration locations during routing
822
818
  let savedScrollPositions: Record<string, number> | null = null;
823
819
  // Externally-provided function to get scroll restoration keys
@@ -894,33 +890,18 @@ export function createRouter(init: RouterInit): Router {
894
890
  // were marked for explicit hydration
895
891
  let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
896
892
  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
-
916
893
  // If errors exist, don't consider routes below the boundary
917
894
  if (errors) {
918
895
  let idx = initialMatches.findIndex(
919
896
  (m) => errors![m.route.id] !== undefined
920
897
  );
921
- initialized = initialMatches.slice(0, idx + 1).every(isRouteInitialized);
898
+ initialized = initialMatches
899
+ .slice(0, idx + 1)
900
+ .every((m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors));
922
901
  } else {
923
- initialized = initialMatches.every(isRouteInitialized);
902
+ initialized = initialMatches.every(
903
+ (m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors)
904
+ );
924
905
  }
925
906
  } else {
926
907
  // Without partial hydration - we're initialized if we were provided any
@@ -1187,8 +1168,8 @@ export function createRouter(init: RouterInit): Router {
1187
1168
  [...subscribers].forEach((subscriber) =>
1188
1169
  subscriber(state, {
1189
1170
  deletedFetchers: deletedFetchersKeys,
1190
- unstable_viewTransitionOpts: opts.viewTransitionOpts,
1191
- unstable_flushSync: opts.flushSync === true,
1171
+ viewTransitionOpts: opts.viewTransitionOpts,
1172
+ flushSync: opts.flushSync === true,
1192
1173
  })
1193
1174
  );
1194
1175
 
@@ -1411,7 +1392,7 @@ export function createRouter(init: RouterInit): Router {
1411
1392
  ? opts.preventScrollReset === true
1412
1393
  : undefined;
1413
1394
 
1414
- let flushSync = (opts && opts.unstable_flushSync) === true;
1395
+ let flushSync = (opts && opts.flushSync) === true;
1415
1396
 
1416
1397
  let blockerKey = shouldBlockNavigation({
1417
1398
  currentLocation,
@@ -1450,7 +1431,7 @@ export function createRouter(init: RouterInit): Router {
1450
1431
  pendingError: error,
1451
1432
  preventScrollReset,
1452
1433
  replace: opts && opts.replace,
1453
- enableViewTransition: opts && opts.unstable_viewTransition,
1434
+ enableViewTransition: opts && opts.viewTransition,
1454
1435
  flushSync,
1455
1436
  });
1456
1437
  }
@@ -1559,7 +1540,7 @@ export function createRouter(init: RouterInit): Router {
1559
1540
  // Short circuit if it's only a hash change and not a revalidation or
1560
1541
  // mutation submission.
1561
1542
  //
1562
- // 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
1563
1544
  // be "same hash". For example, on /page#hash and submit a <Form method="post">
1564
1545
  // which will default to a navigation to /page
1565
1546
  if (
@@ -1709,17 +1690,15 @@ export function createRouter(init: RouterInit): Router {
1709
1690
  if (discoverResult.type === "aborted") {
1710
1691
  return { shortCircuited: true };
1711
1692
  } else if (discoverResult.type === "error") {
1712
- let { boundaryId, error } = handleDiscoverRouteError(
1713
- location.pathname,
1714
- discoverResult
1715
- );
1693
+ let boundaryId = findNearestBoundary(discoverResult.partialMatches)
1694
+ .route.id;
1716
1695
  return {
1717
1696
  matches: discoverResult.partialMatches,
1718
1697
  pendingActionResult: [
1719
1698
  boundaryId,
1720
1699
  {
1721
1700
  type: ResultType.error,
1722
- error,
1701
+ error: discoverResult.error,
1723
1702
  },
1724
1703
  ],
1725
1704
  };
@@ -1887,15 +1866,13 @@ export function createRouter(init: RouterInit): Router {
1887
1866
  if (discoverResult.type === "aborted") {
1888
1867
  return { shortCircuited: true };
1889
1868
  } else if (discoverResult.type === "error") {
1890
- let { boundaryId, error } = handleDiscoverRouteError(
1891
- location.pathname,
1892
- discoverResult
1893
- );
1869
+ let boundaryId = findNearestBoundary(discoverResult.partialMatches)
1870
+ .route.id;
1894
1871
  return {
1895
1872
  matches: discoverResult.partialMatches,
1896
1873
  loaderData: {},
1897
1874
  errors: {
1898
- [boundaryId]: error,
1875
+ [boundaryId]: discoverResult.error,
1899
1876
  },
1900
1877
  };
1901
1878
  } else if (!discoverResult.matches) {
@@ -1983,9 +1960,7 @@ export function createRouter(init: RouterInit): Router {
1983
1960
  }
1984
1961
 
1985
1962
  revalidatingFetchers.forEach((rf) => {
1986
- if (fetchControllers.has(rf.key)) {
1987
- abortFetcher(rf.key);
1988
- }
1963
+ abortFetcher(rf.key);
1989
1964
  if (rf.controller) {
1990
1965
  // Fetchers use an independent AbortController so that aborting a fetcher
1991
1966
  // (via deleteFetcher) does not abort the triggering navigation that
@@ -2026,6 +2001,7 @@ export function createRouter(init: RouterInit): Router {
2026
2001
  abortPendingFetchRevalidations
2027
2002
  );
2028
2003
  }
2004
+
2029
2005
  revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
2030
2006
 
2031
2007
  // If any loaders returned a redirect Response, start a new REPLACE navigation
@@ -2053,7 +2029,6 @@ export function createRouter(init: RouterInit): Router {
2053
2029
  let { loaderData, errors } = processLoaderData(
2054
2030
  state,
2055
2031
  matches,
2056
- matchesToLoad,
2057
2032
  loaderResults,
2058
2033
  pendingActionResult,
2059
2034
  revalidatingFetchers,
@@ -2073,13 +2048,9 @@ export function createRouter(init: RouterInit): Router {
2073
2048
  });
2074
2049
  });
2075
2050
 
2076
- // During partial hydration, preserve SSR errors for routes that don't re-run
2051
+ // Preserve SSR errors during partial hydration
2077
2052
  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
- });
2053
+ errors = { ...state.errors, ...errors };
2083
2054
  }
2084
2055
 
2085
2056
  let updatedFetchers = markFetchRedirectsDone();
@@ -2143,8 +2114,9 @@ export function createRouter(init: RouterInit): Router {
2143
2114
  );
2144
2115
  }
2145
2116
 
2146
- if (fetchControllers.has(key)) abortFetcher(key);
2147
- let flushSync = (opts && opts.unstable_flushSync) === true;
2117
+ abortFetcher(key);
2118
+
2119
+ let flushSync = (opts && opts.flushSync) === true;
2148
2120
 
2149
2121
  let routesToUse = inFlightDataRoutes || dataRoutes;
2150
2122
  let normalizedPath = normalizeTo(
@@ -2188,7 +2160,7 @@ export function createRouter(init: RouterInit): Router {
2188
2160
 
2189
2161
  let match = getTargetMatch(matches, path);
2190
2162
 
2191
- pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2163
+ let preventScrollReset = (opts && opts.preventScrollReset) === true;
2192
2164
 
2193
2165
  if (submission && isMutationMethod(submission.formMethod)) {
2194
2166
  handleFetcherAction(
@@ -2199,6 +2171,7 @@ export function createRouter(init: RouterInit): Router {
2199
2171
  matches,
2200
2172
  fogOfWar.active,
2201
2173
  flushSync,
2174
+ preventScrollReset,
2202
2175
  submission
2203
2176
  );
2204
2177
  return;
@@ -2215,6 +2188,7 @@ export function createRouter(init: RouterInit): Router {
2215
2188
  matches,
2216
2189
  fogOfWar.active,
2217
2190
  flushSync,
2191
+ preventScrollReset,
2218
2192
  submission
2219
2193
  );
2220
2194
  }
@@ -2229,6 +2203,7 @@ export function createRouter(init: RouterInit): Router {
2229
2203
  requestMatches: AgnosticDataRouteMatch[],
2230
2204
  isFogOfWar: boolean,
2231
2205
  flushSync: boolean,
2206
+ preventScrollReset: boolean,
2232
2207
  submission: Submission
2233
2208
  ) {
2234
2209
  interruptActiveLoads();
@@ -2275,8 +2250,7 @@ export function createRouter(init: RouterInit): Router {
2275
2250
  if (discoverResult.type === "aborted") {
2276
2251
  return;
2277
2252
  } else if (discoverResult.type === "error") {
2278
- let { error } = handleDiscoverRouteError(path, discoverResult);
2279
- setFetcherError(key, routeId, error, { flushSync });
2253
+ setFetcherError(key, routeId, discoverResult.error, { flushSync });
2280
2254
  return;
2281
2255
  } else if (!discoverResult.matches) {
2282
2256
  setFetcherError(
@@ -2343,6 +2317,7 @@ export function createRouter(init: RouterInit): Router {
2343
2317
  updateFetcherState(key, getLoadingFetcher(submission));
2344
2318
  return startRedirectNavigation(fetchRequest, actionResult, false, {
2345
2319
  fetcherSubmission: submission,
2320
+ preventScrollReset,
2346
2321
  });
2347
2322
  }
2348
2323
  }
@@ -2412,9 +2387,7 @@ export function createRouter(init: RouterInit): Router {
2412
2387
  existingFetcher ? existingFetcher.data : undefined
2413
2388
  );
2414
2389
  state.fetchers.set(staleKey, revalidatingFetcher);
2415
- if (fetchControllers.has(staleKey)) {
2416
- abortFetcher(staleKey);
2417
- }
2390
+ abortFetcher(staleKey);
2418
2391
  if (rf.controller) {
2419
2392
  fetchControllers.set(staleKey, rf.controller);
2420
2393
  }
@@ -2457,7 +2430,8 @@ export function createRouter(init: RouterInit): Router {
2457
2430
  return startRedirectNavigation(
2458
2431
  revalidationRequest,
2459
2432
  redirect.result,
2460
- false
2433
+ false,
2434
+ { preventScrollReset }
2461
2435
  );
2462
2436
  }
2463
2437
 
@@ -2470,7 +2444,8 @@ export function createRouter(init: RouterInit): Router {
2470
2444
  return startRedirectNavigation(
2471
2445
  revalidationRequest,
2472
2446
  redirect.result,
2473
- false
2447
+ false,
2448
+ { preventScrollReset }
2474
2449
  );
2475
2450
  }
2476
2451
 
@@ -2478,7 +2453,6 @@ export function createRouter(init: RouterInit): Router {
2478
2453
  let { loaderData, errors } = processLoaderData(
2479
2454
  state,
2480
2455
  matches,
2481
- matchesToLoad,
2482
2456
  loaderResults,
2483
2457
  undefined,
2484
2458
  revalidatingFetchers,
@@ -2538,6 +2512,7 @@ export function createRouter(init: RouterInit): Router {
2538
2512
  matches: AgnosticDataRouteMatch[],
2539
2513
  isFogOfWar: boolean,
2540
2514
  flushSync: boolean,
2515
+ preventScrollReset: boolean,
2541
2516
  submission?: Submission
2542
2517
  ) {
2543
2518
  let existingFetcher = state.fetchers.get(key);
@@ -2567,8 +2542,7 @@ export function createRouter(init: RouterInit): Router {
2567
2542
  if (discoverResult.type === "aborted") {
2568
2543
  return;
2569
2544
  } else if (discoverResult.type === "error") {
2570
- let { error } = handleDiscoverRouteError(path, discoverResult);
2571
- setFetcherError(key, routeId, error, { flushSync });
2545
+ setFetcherError(key, routeId, discoverResult.error, { flushSync });
2572
2546
  return;
2573
2547
  } else if (!discoverResult.matches) {
2574
2548
  setFetcherError(
@@ -2634,7 +2608,9 @@ export function createRouter(init: RouterInit): Router {
2634
2608
  return;
2635
2609
  } else {
2636
2610
  fetchRedirectIds.add(key);
2637
- await startRedirectNavigation(fetchRequest, result, false);
2611
+ await startRedirectNavigation(fetchRequest, result, false, {
2612
+ preventScrollReset,
2613
+ });
2638
2614
  return;
2639
2615
  }
2640
2616
  }
@@ -2677,10 +2653,12 @@ export function createRouter(init: RouterInit): Router {
2677
2653
  {
2678
2654
  submission,
2679
2655
  fetcherSubmission,
2656
+ preventScrollReset,
2680
2657
  replace,
2681
2658
  }: {
2682
2659
  submission?: Submission;
2683
2660
  fetcherSubmission?: Submission;
2661
+ preventScrollReset?: boolean;
2684
2662
  replace?: boolean;
2685
2663
  } = {}
2686
2664
  ) {
@@ -2761,7 +2739,7 @@ export function createRouter(init: RouterInit): Router {
2761
2739
  formAction: location,
2762
2740
  },
2763
2741
  // Preserve these flags across redirects
2764
- preventScrollReset: pendingPreventScrollReset,
2742
+ preventScrollReset: preventScrollReset || pendingPreventScrollReset,
2765
2743
  enableViewTransition: isNavigation
2766
2744
  ? pendingViewTransitionEnabled
2767
2745
  : undefined,
@@ -2778,7 +2756,7 @@ export function createRouter(init: RouterInit): Router {
2778
2756
  // Send fetcher submissions through for shouldRevalidate
2779
2757
  fetcherSubmission,
2780
2758
  // Preserve these flags across redirects
2781
- preventScrollReset: pendingPreventScrollReset,
2759
+ preventScrollReset: preventScrollReset || pendingPreventScrollReset,
2782
2760
  enableViewTransition: isNavigation
2783
2761
  ? pendingViewTransitionEnabled
2784
2762
  : undefined,
@@ -2927,8 +2905,8 @@ export function createRouter(init: RouterInit): Router {
2927
2905
  fetchLoadMatches.forEach((_, key) => {
2928
2906
  if (fetchControllers.has(key)) {
2929
2907
  cancelledFetcherLoads.add(key);
2930
- abortFetcher(key);
2931
2908
  }
2909
+ abortFetcher(key);
2932
2910
  });
2933
2911
  }
2934
2912
 
@@ -3011,9 +2989,10 @@ export function createRouter(init: RouterInit): Router {
3011
2989
 
3012
2990
  function abortFetcher(key: string) {
3013
2991
  let controller = fetchControllers.get(key);
3014
- invariant(controller, `Expected fetch controller: ${key}`);
3015
- controller.abort();
3016
- fetchControllers.delete(key);
2992
+ if (controller) {
2993
+ controller.abort();
2994
+ fetchControllers.delete(key);
2995
+ }
3017
2996
  }
3018
2997
 
3019
2998
  function markFetchersDone(keys: string[]) {
@@ -3139,23 +3118,6 @@ export function createRouter(init: RouterInit): Router {
3139
3118
  return { notFoundMatches: matches, route, error };
3140
3119
  }
3141
3120
 
3142
- function handleDiscoverRouteError(
3143
- pathname: string,
3144
- discoverResult: DiscoverRoutesErrorResult
3145
- ) {
3146
- return {
3147
- boundaryId: findNearestBoundary(discoverResult.partialMatches).route.id,
3148
- error: getInternalRouterError(400, {
3149
- type: "route-discovery",
3150
- pathname,
3151
- message:
3152
- discoverResult.error != null && "message" in discoverResult.error
3153
- ? discoverResult.error
3154
- : String(discoverResult.error),
3155
- }),
3156
- };
3157
- }
3158
-
3159
3121
  function cancelActiveDeferreds(
3160
3122
  predicate?: (routeId: string) => boolean
3161
3123
  ): string[] {
@@ -3243,13 +3205,6 @@ export function createRouter(init: RouterInit): Router {
3243
3205
  pathname: string
3244
3206
  ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {
3245
3207
  if (patchRoutesOnNavigationImpl) {
3246
- // Don't bother re-calling patchRouteOnMiss for a path we've already
3247
- // processed. the last execution would have patched the route tree
3248
- // accordingly so `matches` here are already accurate.
3249
- if (discoveredRoutes.has(pathname)) {
3250
- return { active: false, matches };
3251
- }
3252
-
3253
3208
  if (!matches) {
3254
3209
  let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>(
3255
3210
  routesToUse,
@@ -3298,21 +3253,30 @@ export function createRouter(init: RouterInit): Router {
3298
3253
  pathname: string,
3299
3254
  signal: AbortSignal
3300
3255
  ): Promise<DiscoverRoutesResult> {
3256
+ if (!patchRoutesOnNavigationImpl) {
3257
+ return { type: "success", matches };
3258
+ }
3259
+
3301
3260
  let partialMatches: AgnosticDataRouteMatch[] | null = matches;
3302
3261
  while (true) {
3303
3262
  let isNonHMR = inFlightDataRoutes == null;
3304
3263
  let routesToUse = inFlightDataRoutes || dataRoutes;
3264
+ let localManifest = manifest;
3305
3265
  try {
3306
- await loadLazyRouteChildren(
3307
- patchRoutesOnNavigationImpl!,
3308
- pathname,
3309
- partialMatches,
3310
- routesToUse,
3311
- manifest,
3312
- mapRouteProperties,
3313
- pendingPatchRoutes,
3314
- signal
3315
- );
3266
+ await patchRoutesOnNavigationImpl({
3267
+ path: pathname,
3268
+ matches: partialMatches,
3269
+ patch: (routeId, children) => {
3270
+ if (signal.aborted) return;
3271
+ patchRoutesImpl(
3272
+ routeId,
3273
+ children,
3274
+ routesToUse,
3275
+ localManifest,
3276
+ mapRouteProperties
3277
+ );
3278
+ },
3279
+ });
3316
3280
  } catch (e) {
3317
3281
  return { type: "error", error: e, partialMatches };
3318
3282
  } finally {
@@ -3322,7 +3286,7 @@ export function createRouter(init: RouterInit): Router {
3322
3286
  // trigger a re-run of memoized `router.routes` dependencies.
3323
3287
  // HMR will already update the identity and reflow when it lands
3324
3288
  // `inFlightDataRoutes` in `completeNavigation`
3325
- if (isNonHMR) {
3289
+ if (isNonHMR && !signal.aborted) {
3326
3290
  dataRoutes = [...dataRoutes];
3327
3291
  }
3328
3292
  }
@@ -3333,7 +3297,6 @@ export function createRouter(init: RouterInit): Router {
3333
3297
 
3334
3298
  let newMatches = matchRoutes(routesToUse, pathname, basename);
3335
3299
  if (newMatches) {
3336
- addToFifoQueue(pathname, discoveredRoutes);
3337
3300
  return { type: "success", matches: newMatches };
3338
3301
  }
3339
3302
 
@@ -3352,7 +3315,6 @@ export function createRouter(init: RouterInit): Router {
3352
3315
  (m, i) => m.route.id === newPartialMatches![i].route.id
3353
3316
  ))
3354
3317
  ) {
3355
- addToFifoQueue(pathname, discoveredRoutes);
3356
3318
  return { type: "success", matches: null };
3357
3319
  }
3358
3320
 
@@ -3360,14 +3322,6 @@ export function createRouter(init: RouterInit): Router {
3360
3322
  }
3361
3323
  }
3362
3324
 
3363
- function addToFifoQueue(path: string, queue: Set<string>) {
3364
- if (queue.size >= discoveredRoutesMaxSize) {
3365
- let first = queue.values().next().value;
3366
- queue.delete(first);
3367
- }
3368
- queue.add(path);
3369
- }
3370
-
3371
3325
  function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {
3372
3326
  manifest = {};
3373
3327
  inFlightDataRoutes = convertRoutesToDataRoutes(
@@ -3538,11 +3492,11 @@ export function createStaticHandler(
3538
3492
  {
3539
3493
  requestContext,
3540
3494
  skipLoaderErrorBubbling,
3541
- unstable_dataStrategy,
3495
+ dataStrategy,
3542
3496
  }: {
3543
3497
  requestContext?: unknown;
3544
3498
  skipLoaderErrorBubbling?: boolean;
3545
- unstable_dataStrategy?: DataStrategyFunction;
3499
+ dataStrategy?: DataStrategyFunction;
3546
3500
  } = {}
3547
3501
  ): Promise<StaticHandlerContext | Response> {
3548
3502
  let url = new URL(request.url);
@@ -3594,7 +3548,7 @@ export function createStaticHandler(
3594
3548
  location,
3595
3549
  matches,
3596
3550
  requestContext,
3597
- unstable_dataStrategy || null,
3551
+ dataStrategy || null,
3598
3552
  skipLoaderErrorBubbling === true,
3599
3553
  null
3600
3554
  );
@@ -3639,11 +3593,11 @@ export function createStaticHandler(
3639
3593
  {
3640
3594
  routeId,
3641
3595
  requestContext,
3642
- unstable_dataStrategy,
3596
+ dataStrategy,
3643
3597
  }: {
3644
3598
  requestContext?: unknown;
3645
3599
  routeId?: string;
3646
- unstable_dataStrategy?: DataStrategyFunction;
3600
+ dataStrategy?: DataStrategyFunction;
3647
3601
  } = {}
3648
3602
  ): Promise<any> {
3649
3603
  let url = new URL(request.url);
@@ -3677,7 +3631,7 @@ export function createStaticHandler(
3677
3631
  location,
3678
3632
  matches,
3679
3633
  requestContext,
3680
- unstable_dataStrategy || null,
3634
+ dataStrategy || null,
3681
3635
  false,
3682
3636
  match
3683
3637
  );
@@ -3716,7 +3670,7 @@ export function createStaticHandler(
3716
3670
  location: Location,
3717
3671
  matches: AgnosticDataRouteMatch[],
3718
3672
  requestContext: unknown,
3719
- unstable_dataStrategy: DataStrategyFunction | null,
3673
+ dataStrategy: DataStrategyFunction | null,
3720
3674
  skipLoaderErrorBubbling: boolean,
3721
3675
  routeMatch: AgnosticDataRouteMatch | null
3722
3676
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
@@ -3732,7 +3686,7 @@ export function createStaticHandler(
3732
3686
  matches,
3733
3687
  routeMatch || getTargetMatch(matches, location),
3734
3688
  requestContext,
3735
- unstable_dataStrategy,
3689
+ dataStrategy,
3736
3690
  skipLoaderErrorBubbling,
3737
3691
  routeMatch != null
3738
3692
  );
@@ -3743,7 +3697,7 @@ export function createStaticHandler(
3743
3697
  request,
3744
3698
  matches,
3745
3699
  requestContext,
3746
- unstable_dataStrategy,
3700
+ dataStrategy,
3747
3701
  skipLoaderErrorBubbling,
3748
3702
  routeMatch
3749
3703
  );
@@ -3778,7 +3732,7 @@ export function createStaticHandler(
3778
3732
  matches: AgnosticDataRouteMatch[],
3779
3733
  actionMatch: AgnosticDataRouteMatch,
3780
3734
  requestContext: unknown,
3781
- unstable_dataStrategy: DataStrategyFunction | null,
3735
+ dataStrategy: DataStrategyFunction | null,
3782
3736
  skipLoaderErrorBubbling: boolean,
3783
3737
  isRouteRequest: boolean
3784
3738
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
@@ -3805,7 +3759,7 @@ export function createStaticHandler(
3805
3759
  matches,
3806
3760
  isRouteRequest,
3807
3761
  requestContext,
3808
- unstable_dataStrategy
3762
+ dataStrategy
3809
3763
  );
3810
3764
  result = results[actionMatch.route.id];
3811
3765
 
@@ -3877,7 +3831,7 @@ export function createStaticHandler(
3877
3831
  loaderRequest,
3878
3832
  matches,
3879
3833
  requestContext,
3880
- unstable_dataStrategy,
3834
+ dataStrategy,
3881
3835
  skipLoaderErrorBubbling,
3882
3836
  null,
3883
3837
  [boundaryMatch.route.id, result]
@@ -3902,7 +3856,7 @@ export function createStaticHandler(
3902
3856
  loaderRequest,
3903
3857
  matches,
3904
3858
  requestContext,
3905
- unstable_dataStrategy,
3859
+ dataStrategy,
3906
3860
  skipLoaderErrorBubbling,
3907
3861
  null
3908
3862
  );
@@ -3924,7 +3878,7 @@ export function createStaticHandler(
3924
3878
  request: Request,
3925
3879
  matches: AgnosticDataRouteMatch[],
3926
3880
  requestContext: unknown,
3927
- unstable_dataStrategy: DataStrategyFunction | null,
3881
+ dataStrategy: DataStrategyFunction | null,
3928
3882
  skipLoaderErrorBubbling: boolean,
3929
3883
  routeMatch: AgnosticDataRouteMatch | null,
3930
3884
  pendingActionResult?: PendingActionResult
@@ -3987,7 +3941,7 @@ export function createStaticHandler(
3987
3941
  matches,
3988
3942
  isRouteRequest,
3989
3943
  requestContext,
3990
- unstable_dataStrategy
3944
+ dataStrategy
3991
3945
  );
3992
3946
 
3993
3947
  if (request.signal.aborted) {
@@ -4033,10 +3987,10 @@ export function createStaticHandler(
4033
3987
  matches: AgnosticDataRouteMatch[],
4034
3988
  isRouteRequest: boolean,
4035
3989
  requestContext: unknown,
4036
- unstable_dataStrategy: DataStrategyFunction | null
3990
+ dataStrategy: DataStrategyFunction | null
4037
3991
  ): Promise<Record<string, DataResult>> {
4038
3992
  let results = await callDataStrategyImpl(
4039
- unstable_dataStrategy || defaultDataStrategy,
3993
+ dataStrategy || defaultDataStrategy,
4040
3994
  type,
4041
3995
  null,
4042
3996
  request,
@@ -4179,16 +4133,23 @@ function normalizeTo(
4179
4133
  path.hash = location.hash;
4180
4134
  }
4181
4135
 
4182
- // Add an ?index param for matched index routes if we don't already have one
4183
- if (
4184
- (to == null || to === "" || to === ".") &&
4185
- activeRouteMatch &&
4186
- activeRouteMatch.route.index &&
4187
- !hasNakedIndexQuery(path.search)
4188
- ) {
4189
- path.search = path.search
4190
- ? path.search.replace(/^\?/, "?index&")
4191
- : "?index";
4136
+ // Account for `?index` params when routing to the current location
4137
+ if ((to == null || to === "" || to === ".") && activeRouteMatch) {
4138
+ let nakedIndex = hasNakedIndexQuery(path.search);
4139
+ if (activeRouteMatch.route.index && !nakedIndex) {
4140
+ // Add one when we're targeting an index route
4141
+ path.search = path.search
4142
+ ? path.search.replace(/^\?/, "?index&")
4143
+ : "?index";
4144
+ } else if (!activeRouteMatch.route.index && nakedIndex) {
4145
+ // Remove existing ones when we're not
4146
+ let params = new URLSearchParams(path.search);
4147
+ let indexValues = params.getAll("index");
4148
+ params.delete("index");
4149
+ indexValues.filter((v) => v).forEach((v) => params.append("index", v));
4150
+ let qs = params.toString();
4151
+ path.search = qs ? `?${qs}` : "";
4152
+ }
4192
4153
  }
4193
4154
 
4194
4155
  // If we're operating within a basename, prepend it to the pathname. If
@@ -4352,20 +4313,18 @@ function normalizeNavigateOptions(
4352
4313
  return { path: createPath(parsedPath), submission };
4353
4314
  }
4354
4315
 
4355
- // Filter out all routes below any caught error as they aren't going to
4316
+ // Filter out all routes at/below any caught error as they aren't going to
4356
4317
  // render so we don't need to load them
4357
4318
  function getLoaderMatchesUntilBoundary(
4358
4319
  matches: AgnosticDataRouteMatch[],
4359
- boundaryId: string
4320
+ boundaryId: string,
4321
+ includeBoundary = false
4360
4322
  ) {
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
- }
4323
+ let index = matches.findIndex((m) => m.route.id === boundaryId);
4324
+ if (index >= 0) {
4325
+ return matches.slice(0, includeBoundary ? index + 1 : index);
4367
4326
  }
4368
- return boundaryMatches;
4327
+ return matches;
4369
4328
  }
4370
4329
 
4371
4330
  function getMatchesToLoad(
@@ -4374,7 +4333,7 @@ function getMatchesToLoad(
4374
4333
  matches: AgnosticDataRouteMatch[],
4375
4334
  submission: Submission | undefined,
4376
4335
  location: Location,
4377
- isInitialLoad: boolean,
4336
+ initialHydration: boolean,
4378
4337
  skipActionErrorRevalidation: boolean,
4379
4338
  isRevalidationRequired: boolean,
4380
4339
  cancelledDeferredRoutes: string[],
@@ -4395,13 +4354,26 @@ function getMatchesToLoad(
4395
4354
  let nextUrl = history.createURL(location);
4396
4355
 
4397
4356
  // 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;
4357
+ let boundaryMatches = matches;
4358
+ if (initialHydration && state.errors) {
4359
+ // On initial hydration, only consider matches up to _and including_ the boundary.
4360
+ // This is inclusive to handle cases where a server loader ran successfully,
4361
+ // a child server loader bubbled up to this route, but this route has
4362
+ // `clientLoader.hydrate` so we want to still run the `clientLoader` so that
4363
+ // we have a complete version of `loaderData`
4364
+ boundaryMatches = getLoaderMatchesUntilBoundary(
4365
+ matches,
4366
+ Object.keys(state.errors)[0],
4367
+ true
4368
+ );
4369
+ } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) {
4370
+ // If an action threw an error, we call loaders up to, but not including the
4371
+ // boundary
4372
+ boundaryMatches = getLoaderMatchesUntilBoundary(
4373
+ matches,
4374
+ pendingActionResult[0]
4375
+ );
4376
+ }
4405
4377
 
4406
4378
  // Don't revalidate loaders by default after action 4xx/5xx responses
4407
4379
  // when the flag is enabled. They can still opt-into revalidation via
@@ -4423,15 +4395,8 @@ function getMatchesToLoad(
4423
4395
  return false;
4424
4396
  }
4425
4397
 
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
- );
4398
+ if (initialHydration) {
4399
+ return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
4435
4400
  }
4436
4401
 
4437
4402
  // Always call the loader on new route instances and pending defer cancellations
@@ -4473,12 +4438,12 @@ function getMatchesToLoad(
4473
4438
  let revalidatingFetchers: RevalidatingFetcher[] = [];
4474
4439
  fetchLoadMatches.forEach((f, key) => {
4475
4440
  // Don't revalidate:
4476
- // - on initial load (shouldn't be any fetchers then anyway)
4441
+ // - on initial hydration (shouldn't be any fetchers then anyway)
4477
4442
  // - if fetcher won't be present in the subsequent render
4478
4443
  // - no longer matches the URL (v7_fetcherPersist=false)
4479
4444
  // - was unmounted but persisted due to v7_fetcherPersist=true
4480
4445
  if (
4481
- isInitialLoad ||
4446
+ initialHydration ||
4482
4447
  !matches.some((m) => m.route.id === f.routeId) ||
4483
4448
  deletedFetchers.has(key)
4484
4449
  ) {
@@ -4558,6 +4523,38 @@ function getMatchesToLoad(
4558
4523
  return [navigationMatches, revalidatingFetchers];
4559
4524
  }
4560
4525
 
4526
+ function shouldLoadRouteOnHydration(
4527
+ route: AgnosticDataRouteObject,
4528
+ loaderData: RouteData | null | undefined,
4529
+ errors: RouteData | null | undefined
4530
+ ) {
4531
+ // We dunno if we have a loader - gotta find out!
4532
+ if (route.lazy) {
4533
+ return true;
4534
+ }
4535
+
4536
+ // No loader, nothing to initialize
4537
+ if (!route.loader) {
4538
+ return false;
4539
+ }
4540
+
4541
+ let hasData = loaderData != null && loaderData[route.id] !== undefined;
4542
+ let hasError = errors != null && errors[route.id] !== undefined;
4543
+
4544
+ // Don't run if we error'd during SSR
4545
+ if (!hasData && hasError) {
4546
+ return false;
4547
+ }
4548
+
4549
+ // Explicitly opting-in to running on hydration
4550
+ if (typeof route.loader === "function" && route.loader.hydrate === true) {
4551
+ return true;
4552
+ }
4553
+
4554
+ // Otherwise, run if we're not yet initialized with anything
4555
+ return !hasData && !hasError;
4556
+ }
4557
+
4561
4558
  function isNewLoader(
4562
4559
  currentLoaderData: RouteData,
4563
4560
  currentMatch: AgnosticDataRouteMatch,
@@ -4607,53 +4604,6 @@ function shouldRevalidateLoader(
4607
4604
  return arg.defaultShouldRevalidate;
4608
4605
  }
4609
4606
 
4610
- /**
4611
- * Idempotent utility to execute patchRoutesOnNavigation() to lazily load route
4612
- * definitions and update the routes/routeManifest
4613
- */
4614
- async function loadLazyRouteChildren(
4615
- patchRoutesOnNavigationImpl: AgnosticPatchRoutesOnNavigationFunction,
4616
- path: string,
4617
- matches: AgnosticDataRouteMatch[],
4618
- routes: AgnosticDataRouteObject[],
4619
- manifest: RouteManifest,
4620
- mapRouteProperties: MapRoutePropertiesFunction,
4621
- pendingRouteChildren: Map<
4622
- string,
4623
- ReturnType<typeof patchRoutesOnNavigationImpl>
4624
- >,
4625
- signal: AbortSignal
4626
- ) {
4627
- let key = [path, ...matches.map((m) => m.route.id)].join("-");
4628
- try {
4629
- let pending = pendingRouteChildren.get(key);
4630
- if (!pending) {
4631
- pending = patchRoutesOnNavigationImpl({
4632
- path,
4633
- matches,
4634
- patch: (routeId, children) => {
4635
- if (!signal.aborted) {
4636
- patchRoutesImpl(
4637
- routeId,
4638
- children,
4639
- routes,
4640
- manifest,
4641
- mapRouteProperties
4642
- );
4643
- }
4644
- },
4645
- });
4646
- pendingRouteChildren.set(key, pending);
4647
- }
4648
-
4649
- if (pending && isPromise<AgnosticRouteObject[]>(pending)) {
4650
- await pending;
4651
- }
4652
- } finally {
4653
- pendingRouteChildren.delete(key);
4654
- }
4655
- }
4656
-
4657
4607
  function patchRoutesImpl(
4658
4608
  routeId: string | null,
4659
4609
  children: AgnosticRouteObject[],
@@ -4661,32 +4611,79 @@ function patchRoutesImpl(
4661
4611
  manifest: RouteManifest,
4662
4612
  mapRouteProperties: MapRoutePropertiesFunction
4663
4613
  ) {
4614
+ let childrenToPatch: AgnosticDataRouteObject[];
4664
4615
  if (routeId) {
4665
4616
  let route = manifest[routeId];
4666
4617
  invariant(
4667
4618
  route,
4668
4619
  `No route found to patch children into: routeId = ${routeId}`
4669
4620
  );
4670
- let dataChildren = convertRoutesToDataRoutes(
4671
- children,
4672
- mapRouteProperties,
4673
- [routeId, "patch", String(route.children?.length || "0")],
4674
- manifest
4675
- );
4676
- if (route.children) {
4677
- route.children.push(...dataChildren);
4678
- } else {
4679
- route.children = dataChildren;
4621
+ if (!route.children) {
4622
+ route.children = [];
4680
4623
  }
4624
+ childrenToPatch = route.children;
4681
4625
  } else {
4682
- let dataChildren = convertRoutesToDataRoutes(
4683
- children,
4684
- mapRouteProperties,
4685
- ["patch", String(routesToUse.length || "0")],
4686
- manifest
4687
- );
4688
- routesToUse.push(...dataChildren);
4626
+ childrenToPatch = routesToUse;
4689
4627
  }
4628
+
4629
+ // Don't patch in routes we already know about so that `patch` is idempotent
4630
+ // to simplify user-land code. This is useful because we re-call the
4631
+ // `patchRoutesOnNavigation` function for matched routes with params.
4632
+ let uniqueChildren = children.filter(
4633
+ (newRoute) =>
4634
+ !childrenToPatch.some((existingRoute) =>
4635
+ isSameRoute(newRoute, existingRoute)
4636
+ )
4637
+ );
4638
+
4639
+ let newRoutes = convertRoutesToDataRoutes(
4640
+ uniqueChildren,
4641
+ mapRouteProperties,
4642
+ [routeId || "_", "patch", String(childrenToPatch?.length || "0")],
4643
+ manifest
4644
+ );
4645
+
4646
+ childrenToPatch.push(...newRoutes);
4647
+ }
4648
+
4649
+ function isSameRoute(
4650
+ newRoute: AgnosticRouteObject,
4651
+ existingRoute: AgnosticRouteObject
4652
+ ): boolean {
4653
+ // Most optimal check is by id
4654
+ if (
4655
+ "id" in newRoute &&
4656
+ "id" in existingRoute &&
4657
+ newRoute.id === existingRoute.id
4658
+ ) {
4659
+ return true;
4660
+ }
4661
+
4662
+ // Second is by pathing differences
4663
+ if (
4664
+ !(
4665
+ newRoute.index === existingRoute.index &&
4666
+ newRoute.path === existingRoute.path &&
4667
+ newRoute.caseSensitive === existingRoute.caseSensitive
4668
+ )
4669
+ ) {
4670
+ return false;
4671
+ }
4672
+
4673
+ // Pathless layout routes are trickier since we need to check children.
4674
+ // If they have no children then they're the same as far as we can tell
4675
+ if (
4676
+ (!newRoute.children || newRoute.children.length === 0) &&
4677
+ (!existingRoute.children || existingRoute.children.length === 0)
4678
+ ) {
4679
+ return true;
4680
+ }
4681
+
4682
+ // Otherwise, we look to see if every child in the new route is already
4683
+ // represented in the existing route's children
4684
+ return newRoute.children!.every((aChild, i) =>
4685
+ existingRoute.children?.some((bChild) => isSameRoute(aChild, bChild))
4686
+ );
4690
4687
  }
4691
4688
 
4692
4689
  /**
@@ -5038,7 +5035,7 @@ async function convertDataStrategyResultToDataResult(
5038
5035
  };
5039
5036
  }
5040
5037
 
5041
- // Convert thrown unstable_data() to ErrorResponse instances
5038
+ // Convert thrown data() to ErrorResponse instances
5042
5039
  result = new ErrorResponseImpl(
5043
5040
  result.init?.status || 500,
5044
5041
  undefined,
@@ -5310,7 +5307,6 @@ function processRouteLoaderData(
5310
5307
  function processLoaderData(
5311
5308
  state: RouterState,
5312
5309
  matches: AgnosticDataRouteMatch[],
5313
- matchesToLoad: AgnosticDataRouteMatch[],
5314
5310
  results: Record<string, DataResult>,
5315
5311
  pendingActionResult: PendingActionResult | undefined,
5316
5312
  revalidatingFetchers: RevalidatingFetcher[],
@@ -5466,7 +5462,7 @@ function getInternalRouterError(
5466
5462
  pathname?: string;
5467
5463
  routeId?: string;
5468
5464
  method?: string;
5469
- type?: "defer-action" | "invalid-body" | "route-discovery";
5465
+ type?: "defer-action" | "invalid-body";
5470
5466
  message?: string;
5471
5467
  } = {}
5472
5468
  ) {
@@ -5475,11 +5471,7 @@ function getInternalRouterError(
5475
5471
 
5476
5472
  if (status === 400) {
5477
5473
  statusText = "Bad Request";
5478
- if (type === "route-discovery") {
5479
- errorMessage =
5480
- `Unable to match URL "${pathname}" - the \`unstable_patchRoutesOnNavigation()\` ` +
5481
- `function threw the following error:\n${message}`;
5482
- } else if (method && pathname && routeId) {
5474
+ if (method && pathname && routeId) {
5483
5475
  errorMessage =
5484
5476
  `You made a ${method} request to "${pathname}" but ` +
5485
5477
  `did not provide a \`loader\` for route "${routeId}", ` +