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

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 (
@@ -1983,9 +1964,7 @@ export function createRouter(init: RouterInit): Router {
1983
1964
  }
1984
1965
 
1985
1966
  revalidatingFetchers.forEach((rf) => {
1986
- if (fetchControllers.has(rf.key)) {
1987
- abortFetcher(rf.key);
1988
- }
1967
+ abortFetcher(rf.key);
1989
1968
  if (rf.controller) {
1990
1969
  // Fetchers use an independent AbortController so that aborting a fetcher
1991
1970
  // (via deleteFetcher) does not abort the triggering navigation that
@@ -2026,6 +2005,7 @@ export function createRouter(init: RouterInit): Router {
2026
2005
  abortPendingFetchRevalidations
2027
2006
  );
2028
2007
  }
2008
+
2029
2009
  revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
2030
2010
 
2031
2011
  // If any loaders returned a redirect Response, start a new REPLACE navigation
@@ -2053,7 +2033,6 @@ export function createRouter(init: RouterInit): Router {
2053
2033
  let { loaderData, errors } = processLoaderData(
2054
2034
  state,
2055
2035
  matches,
2056
- matchesToLoad,
2057
2036
  loaderResults,
2058
2037
  pendingActionResult,
2059
2038
  revalidatingFetchers,
@@ -2073,13 +2052,9 @@ export function createRouter(init: RouterInit): Router {
2073
2052
  });
2074
2053
  });
2075
2054
 
2076
- // During partial hydration, preserve SSR errors for routes that don't re-run
2055
+ // Preserve SSR errors during partial hydration
2077
2056
  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
- });
2057
+ errors = { ...state.errors, ...errors };
2083
2058
  }
2084
2059
 
2085
2060
  let updatedFetchers = markFetchRedirectsDone();
@@ -2143,8 +2118,9 @@ export function createRouter(init: RouterInit): Router {
2143
2118
  );
2144
2119
  }
2145
2120
 
2146
- if (fetchControllers.has(key)) abortFetcher(key);
2147
- let flushSync = (opts && opts.unstable_flushSync) === true;
2121
+ abortFetcher(key);
2122
+
2123
+ let flushSync = (opts && opts.flushSync) === true;
2148
2124
 
2149
2125
  let routesToUse = inFlightDataRoutes || dataRoutes;
2150
2126
  let normalizedPath = normalizeTo(
@@ -2188,7 +2164,7 @@ export function createRouter(init: RouterInit): Router {
2188
2164
 
2189
2165
  let match = getTargetMatch(matches, path);
2190
2166
 
2191
- pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2167
+ let preventScrollReset = (opts && opts.preventScrollReset) === true;
2192
2168
 
2193
2169
  if (submission && isMutationMethod(submission.formMethod)) {
2194
2170
  handleFetcherAction(
@@ -2199,6 +2175,7 @@ export function createRouter(init: RouterInit): Router {
2199
2175
  matches,
2200
2176
  fogOfWar.active,
2201
2177
  flushSync,
2178
+ preventScrollReset,
2202
2179
  submission
2203
2180
  );
2204
2181
  return;
@@ -2215,6 +2192,7 @@ export function createRouter(init: RouterInit): Router {
2215
2192
  matches,
2216
2193
  fogOfWar.active,
2217
2194
  flushSync,
2195
+ preventScrollReset,
2218
2196
  submission
2219
2197
  );
2220
2198
  }
@@ -2229,6 +2207,7 @@ export function createRouter(init: RouterInit): Router {
2229
2207
  requestMatches: AgnosticDataRouteMatch[],
2230
2208
  isFogOfWar: boolean,
2231
2209
  flushSync: boolean,
2210
+ preventScrollReset: boolean,
2232
2211
  submission: Submission
2233
2212
  ) {
2234
2213
  interruptActiveLoads();
@@ -2343,6 +2322,7 @@ export function createRouter(init: RouterInit): Router {
2343
2322
  updateFetcherState(key, getLoadingFetcher(submission));
2344
2323
  return startRedirectNavigation(fetchRequest, actionResult, false, {
2345
2324
  fetcherSubmission: submission,
2325
+ preventScrollReset,
2346
2326
  });
2347
2327
  }
2348
2328
  }
@@ -2412,9 +2392,7 @@ export function createRouter(init: RouterInit): Router {
2412
2392
  existingFetcher ? existingFetcher.data : undefined
2413
2393
  );
2414
2394
  state.fetchers.set(staleKey, revalidatingFetcher);
2415
- if (fetchControllers.has(staleKey)) {
2416
- abortFetcher(staleKey);
2417
- }
2395
+ abortFetcher(staleKey);
2418
2396
  if (rf.controller) {
2419
2397
  fetchControllers.set(staleKey, rf.controller);
2420
2398
  }
@@ -2457,7 +2435,8 @@ export function createRouter(init: RouterInit): Router {
2457
2435
  return startRedirectNavigation(
2458
2436
  revalidationRequest,
2459
2437
  redirect.result,
2460
- false
2438
+ false,
2439
+ { preventScrollReset }
2461
2440
  );
2462
2441
  }
2463
2442
 
@@ -2470,7 +2449,8 @@ export function createRouter(init: RouterInit): Router {
2470
2449
  return startRedirectNavigation(
2471
2450
  revalidationRequest,
2472
2451
  redirect.result,
2473
- false
2452
+ false,
2453
+ { preventScrollReset }
2474
2454
  );
2475
2455
  }
2476
2456
 
@@ -2478,7 +2458,6 @@ export function createRouter(init: RouterInit): Router {
2478
2458
  let { loaderData, errors } = processLoaderData(
2479
2459
  state,
2480
2460
  matches,
2481
- matchesToLoad,
2482
2461
  loaderResults,
2483
2462
  undefined,
2484
2463
  revalidatingFetchers,
@@ -2538,6 +2517,7 @@ export function createRouter(init: RouterInit): Router {
2538
2517
  matches: AgnosticDataRouteMatch[],
2539
2518
  isFogOfWar: boolean,
2540
2519
  flushSync: boolean,
2520
+ preventScrollReset: boolean,
2541
2521
  submission?: Submission
2542
2522
  ) {
2543
2523
  let existingFetcher = state.fetchers.get(key);
@@ -2634,7 +2614,9 @@ export function createRouter(init: RouterInit): Router {
2634
2614
  return;
2635
2615
  } else {
2636
2616
  fetchRedirectIds.add(key);
2637
- await startRedirectNavigation(fetchRequest, result, false);
2617
+ await startRedirectNavigation(fetchRequest, result, false, {
2618
+ preventScrollReset,
2619
+ });
2638
2620
  return;
2639
2621
  }
2640
2622
  }
@@ -2677,10 +2659,12 @@ export function createRouter(init: RouterInit): Router {
2677
2659
  {
2678
2660
  submission,
2679
2661
  fetcherSubmission,
2662
+ preventScrollReset,
2680
2663
  replace,
2681
2664
  }: {
2682
2665
  submission?: Submission;
2683
2666
  fetcherSubmission?: Submission;
2667
+ preventScrollReset?: boolean;
2684
2668
  replace?: boolean;
2685
2669
  } = {}
2686
2670
  ) {
@@ -2761,7 +2745,7 @@ export function createRouter(init: RouterInit): Router {
2761
2745
  formAction: location,
2762
2746
  },
2763
2747
  // Preserve these flags across redirects
2764
- preventScrollReset: pendingPreventScrollReset,
2748
+ preventScrollReset: preventScrollReset || pendingPreventScrollReset,
2765
2749
  enableViewTransition: isNavigation
2766
2750
  ? pendingViewTransitionEnabled
2767
2751
  : undefined,
@@ -2778,7 +2762,7 @@ export function createRouter(init: RouterInit): Router {
2778
2762
  // Send fetcher submissions through for shouldRevalidate
2779
2763
  fetcherSubmission,
2780
2764
  // Preserve these flags across redirects
2781
- preventScrollReset: pendingPreventScrollReset,
2765
+ preventScrollReset: preventScrollReset || pendingPreventScrollReset,
2782
2766
  enableViewTransition: isNavigation
2783
2767
  ? pendingViewTransitionEnabled
2784
2768
  : undefined,
@@ -2927,8 +2911,8 @@ export function createRouter(init: RouterInit): Router {
2927
2911
  fetchLoadMatches.forEach((_, key) => {
2928
2912
  if (fetchControllers.has(key)) {
2929
2913
  cancelledFetcherLoads.add(key);
2930
- abortFetcher(key);
2931
2914
  }
2915
+ abortFetcher(key);
2932
2916
  });
2933
2917
  }
2934
2918
 
@@ -3011,9 +2995,10 @@ export function createRouter(init: RouterInit): Router {
3011
2995
 
3012
2996
  function abortFetcher(key: string) {
3013
2997
  let controller = fetchControllers.get(key);
3014
- invariant(controller, `Expected fetch controller: ${key}`);
3015
- controller.abort();
3016
- fetchControllers.delete(key);
2998
+ if (controller) {
2999
+ controller.abort();
3000
+ fetchControllers.delete(key);
3001
+ }
3017
3002
  }
3018
3003
 
3019
3004
  function markFetchersDone(keys: string[]) {
@@ -3243,13 +3228,6 @@ export function createRouter(init: RouterInit): Router {
3243
3228
  pathname: string
3244
3229
  ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {
3245
3230
  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
3231
  if (!matches) {
3254
3232
  let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>(
3255
3233
  routesToUse,
@@ -3298,21 +3276,30 @@ export function createRouter(init: RouterInit): Router {
3298
3276
  pathname: string,
3299
3277
  signal: AbortSignal
3300
3278
  ): Promise<DiscoverRoutesResult> {
3279
+ if (!patchRoutesOnNavigationImpl) {
3280
+ return { type: "success", matches };
3281
+ }
3282
+
3301
3283
  let partialMatches: AgnosticDataRouteMatch[] | null = matches;
3302
3284
  while (true) {
3303
3285
  let isNonHMR = inFlightDataRoutes == null;
3304
3286
  let routesToUse = inFlightDataRoutes || dataRoutes;
3287
+ let localManifest = manifest;
3305
3288
  try {
3306
- await loadLazyRouteChildren(
3307
- patchRoutesOnNavigationImpl!,
3308
- pathname,
3309
- partialMatches,
3310
- routesToUse,
3311
- manifest,
3312
- mapRouteProperties,
3313
- pendingPatchRoutes,
3314
- signal
3315
- );
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
+ });
3316
3303
  } catch (e) {
3317
3304
  return { type: "error", error: e, partialMatches };
3318
3305
  } finally {
@@ -3322,7 +3309,7 @@ export function createRouter(init: RouterInit): Router {
3322
3309
  // trigger a re-run of memoized `router.routes` dependencies.
3323
3310
  // HMR will already update the identity and reflow when it lands
3324
3311
  // `inFlightDataRoutes` in `completeNavigation`
3325
- if (isNonHMR) {
3312
+ if (isNonHMR && !signal.aborted) {
3326
3313
  dataRoutes = [...dataRoutes];
3327
3314
  }
3328
3315
  }
@@ -3333,7 +3320,6 @@ export function createRouter(init: RouterInit): Router {
3333
3320
 
3334
3321
  let newMatches = matchRoutes(routesToUse, pathname, basename);
3335
3322
  if (newMatches) {
3336
- addToFifoQueue(pathname, discoveredRoutes);
3337
3323
  return { type: "success", matches: newMatches };
3338
3324
  }
3339
3325
 
@@ -3352,7 +3338,6 @@ export function createRouter(init: RouterInit): Router {
3352
3338
  (m, i) => m.route.id === newPartialMatches![i].route.id
3353
3339
  ))
3354
3340
  ) {
3355
- addToFifoQueue(pathname, discoveredRoutes);
3356
3341
  return { type: "success", matches: null };
3357
3342
  }
3358
3343
 
@@ -3360,14 +3345,6 @@ export function createRouter(init: RouterInit): Router {
3360
3345
  }
3361
3346
  }
3362
3347
 
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
3348
  function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {
3372
3349
  manifest = {};
3373
3350
  inFlightDataRoutes = convertRoutesToDataRoutes(
@@ -3538,11 +3515,11 @@ export function createStaticHandler(
3538
3515
  {
3539
3516
  requestContext,
3540
3517
  skipLoaderErrorBubbling,
3541
- unstable_dataStrategy,
3518
+ dataStrategy,
3542
3519
  }: {
3543
3520
  requestContext?: unknown;
3544
3521
  skipLoaderErrorBubbling?: boolean;
3545
- unstable_dataStrategy?: DataStrategyFunction;
3522
+ dataStrategy?: DataStrategyFunction;
3546
3523
  } = {}
3547
3524
  ): Promise<StaticHandlerContext | Response> {
3548
3525
  let url = new URL(request.url);
@@ -3594,7 +3571,7 @@ export function createStaticHandler(
3594
3571
  location,
3595
3572
  matches,
3596
3573
  requestContext,
3597
- unstable_dataStrategy || null,
3574
+ dataStrategy || null,
3598
3575
  skipLoaderErrorBubbling === true,
3599
3576
  null
3600
3577
  );
@@ -3639,11 +3616,11 @@ export function createStaticHandler(
3639
3616
  {
3640
3617
  routeId,
3641
3618
  requestContext,
3642
- unstable_dataStrategy,
3619
+ dataStrategy,
3643
3620
  }: {
3644
3621
  requestContext?: unknown;
3645
3622
  routeId?: string;
3646
- unstable_dataStrategy?: DataStrategyFunction;
3623
+ dataStrategy?: DataStrategyFunction;
3647
3624
  } = {}
3648
3625
  ): Promise<any> {
3649
3626
  let url = new URL(request.url);
@@ -3677,7 +3654,7 @@ export function createStaticHandler(
3677
3654
  location,
3678
3655
  matches,
3679
3656
  requestContext,
3680
- unstable_dataStrategy || null,
3657
+ dataStrategy || null,
3681
3658
  false,
3682
3659
  match
3683
3660
  );
@@ -3716,7 +3693,7 @@ export function createStaticHandler(
3716
3693
  location: Location,
3717
3694
  matches: AgnosticDataRouteMatch[],
3718
3695
  requestContext: unknown,
3719
- unstable_dataStrategy: DataStrategyFunction | null,
3696
+ dataStrategy: DataStrategyFunction | null,
3720
3697
  skipLoaderErrorBubbling: boolean,
3721
3698
  routeMatch: AgnosticDataRouteMatch | null
3722
3699
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
@@ -3732,7 +3709,7 @@ export function createStaticHandler(
3732
3709
  matches,
3733
3710
  routeMatch || getTargetMatch(matches, location),
3734
3711
  requestContext,
3735
- unstable_dataStrategy,
3712
+ dataStrategy,
3736
3713
  skipLoaderErrorBubbling,
3737
3714
  routeMatch != null
3738
3715
  );
@@ -3743,7 +3720,7 @@ export function createStaticHandler(
3743
3720
  request,
3744
3721
  matches,
3745
3722
  requestContext,
3746
- unstable_dataStrategy,
3723
+ dataStrategy,
3747
3724
  skipLoaderErrorBubbling,
3748
3725
  routeMatch
3749
3726
  );
@@ -3778,7 +3755,7 @@ export function createStaticHandler(
3778
3755
  matches: AgnosticDataRouteMatch[],
3779
3756
  actionMatch: AgnosticDataRouteMatch,
3780
3757
  requestContext: unknown,
3781
- unstable_dataStrategy: DataStrategyFunction | null,
3758
+ dataStrategy: DataStrategyFunction | null,
3782
3759
  skipLoaderErrorBubbling: boolean,
3783
3760
  isRouteRequest: boolean
3784
3761
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
@@ -3805,7 +3782,7 @@ export function createStaticHandler(
3805
3782
  matches,
3806
3783
  isRouteRequest,
3807
3784
  requestContext,
3808
- unstable_dataStrategy
3785
+ dataStrategy
3809
3786
  );
3810
3787
  result = results[actionMatch.route.id];
3811
3788
 
@@ -3877,7 +3854,7 @@ export function createStaticHandler(
3877
3854
  loaderRequest,
3878
3855
  matches,
3879
3856
  requestContext,
3880
- unstable_dataStrategy,
3857
+ dataStrategy,
3881
3858
  skipLoaderErrorBubbling,
3882
3859
  null,
3883
3860
  [boundaryMatch.route.id, result]
@@ -3902,7 +3879,7 @@ export function createStaticHandler(
3902
3879
  loaderRequest,
3903
3880
  matches,
3904
3881
  requestContext,
3905
- unstable_dataStrategy,
3882
+ dataStrategy,
3906
3883
  skipLoaderErrorBubbling,
3907
3884
  null
3908
3885
  );
@@ -3924,7 +3901,7 @@ export function createStaticHandler(
3924
3901
  request: Request,
3925
3902
  matches: AgnosticDataRouteMatch[],
3926
3903
  requestContext: unknown,
3927
- unstable_dataStrategy: DataStrategyFunction | null,
3904
+ dataStrategy: DataStrategyFunction | null,
3928
3905
  skipLoaderErrorBubbling: boolean,
3929
3906
  routeMatch: AgnosticDataRouteMatch | null,
3930
3907
  pendingActionResult?: PendingActionResult
@@ -3987,7 +3964,7 @@ export function createStaticHandler(
3987
3964
  matches,
3988
3965
  isRouteRequest,
3989
3966
  requestContext,
3990
- unstable_dataStrategy
3967
+ dataStrategy
3991
3968
  );
3992
3969
 
3993
3970
  if (request.signal.aborted) {
@@ -4033,10 +4010,10 @@ export function createStaticHandler(
4033
4010
  matches: AgnosticDataRouteMatch[],
4034
4011
  isRouteRequest: boolean,
4035
4012
  requestContext: unknown,
4036
- unstable_dataStrategy: DataStrategyFunction | null
4013
+ dataStrategy: DataStrategyFunction | null
4037
4014
  ): Promise<Record<string, DataResult>> {
4038
4015
  let results = await callDataStrategyImpl(
4039
- unstable_dataStrategy || defaultDataStrategy,
4016
+ dataStrategy || defaultDataStrategy,
4040
4017
  type,
4041
4018
  null,
4042
4019
  request,
@@ -4179,16 +4156,23 @@ function normalizeTo(
4179
4156
  path.hash = location.hash;
4180
4157
  }
4181
4158
 
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";
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
+ }
4192
4176
  }
4193
4177
 
4194
4178
  // If we're operating within a basename, prepend it to the pathname. If
@@ -4352,20 +4336,18 @@ function normalizeNavigateOptions(
4352
4336
  return { path: createPath(parsedPath), submission };
4353
4337
  }
4354
4338
 
4355
- // 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
4356
4340
  // render so we don't need to load them
4357
4341
  function getLoaderMatchesUntilBoundary(
4358
4342
  matches: AgnosticDataRouteMatch[],
4359
- boundaryId: string
4343
+ boundaryId: string,
4344
+ includeBoundary = false
4360
4345
  ) {
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
- }
4346
+ let index = matches.findIndex((m) => m.route.id === boundaryId);
4347
+ if (index >= 0) {
4348
+ return matches.slice(0, includeBoundary ? index + 1 : index);
4367
4349
  }
4368
- return boundaryMatches;
4350
+ return matches;
4369
4351
  }
4370
4352
 
4371
4353
  function getMatchesToLoad(
@@ -4374,7 +4356,7 @@ function getMatchesToLoad(
4374
4356
  matches: AgnosticDataRouteMatch[],
4375
4357
  submission: Submission | undefined,
4376
4358
  location: Location,
4377
- isInitialLoad: boolean,
4359
+ initialHydration: boolean,
4378
4360
  skipActionErrorRevalidation: boolean,
4379
4361
  isRevalidationRequired: boolean,
4380
4362
  cancelledDeferredRoutes: string[],
@@ -4395,13 +4377,26 @@ function getMatchesToLoad(
4395
4377
  let nextUrl = history.createURL(location);
4396
4378
 
4397
4379
  // 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;
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
+ }
4405
4400
 
4406
4401
  // Don't revalidate loaders by default after action 4xx/5xx responses
4407
4402
  // when the flag is enabled. They can still opt-into revalidation via
@@ -4423,15 +4418,8 @@ function getMatchesToLoad(
4423
4418
  return false;
4424
4419
  }
4425
4420
 
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
- );
4421
+ if (initialHydration) {
4422
+ return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
4435
4423
  }
4436
4424
 
4437
4425
  // Always call the loader on new route instances and pending defer cancellations
@@ -4473,12 +4461,12 @@ function getMatchesToLoad(
4473
4461
  let revalidatingFetchers: RevalidatingFetcher[] = [];
4474
4462
  fetchLoadMatches.forEach((f, key) => {
4475
4463
  // Don't revalidate:
4476
- // - on initial load (shouldn't be any fetchers then anyway)
4464
+ // - on initial hydration (shouldn't be any fetchers then anyway)
4477
4465
  // - if fetcher won't be present in the subsequent render
4478
4466
  // - no longer matches the URL (v7_fetcherPersist=false)
4479
4467
  // - was unmounted but persisted due to v7_fetcherPersist=true
4480
4468
  if (
4481
- isInitialLoad ||
4469
+ initialHydration ||
4482
4470
  !matches.some((m) => m.route.id === f.routeId) ||
4483
4471
  deletedFetchers.has(key)
4484
4472
  ) {
@@ -4558,6 +4546,38 @@ function getMatchesToLoad(
4558
4546
  return [navigationMatches, revalidatingFetchers];
4559
4547
  }
4560
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
+
4561
4581
  function isNewLoader(
4562
4582
  currentLoaderData: RouteData,
4563
4583
  currentMatch: AgnosticDataRouteMatch,
@@ -4607,53 +4627,6 @@ function shouldRevalidateLoader(
4607
4627
  return arg.defaultShouldRevalidate;
4608
4628
  }
4609
4629
 
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
4630
  function patchRoutesImpl(
4658
4631
  routeId: string | null,
4659
4632
  children: AgnosticRouteObject[],
@@ -4661,32 +4634,79 @@ function patchRoutesImpl(
4661
4634
  manifest: RouteManifest,
4662
4635
  mapRouteProperties: MapRoutePropertiesFunction
4663
4636
  ) {
4637
+ let childrenToPatch: AgnosticDataRouteObject[];
4664
4638
  if (routeId) {
4665
4639
  let route = manifest[routeId];
4666
4640
  invariant(
4667
4641
  route,
4668
4642
  `No route found to patch children into: routeId = ${routeId}`
4669
4643
  );
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;
4644
+ if (!route.children) {
4645
+ route.children = [];
4680
4646
  }
4647
+ childrenToPatch = route.children;
4681
4648
  } else {
4682
- let dataChildren = convertRoutesToDataRoutes(
4683
- children,
4684
- mapRouteProperties,
4685
- ["patch", String(routesToUse.length || "0")],
4686
- manifest
4687
- );
4688
- routesToUse.push(...dataChildren);
4649
+ childrenToPatch = routesToUse;
4650
+ }
4651
+
4652
+ // Don't patch in routes we already know about so that `patch` is idempotent
4653
+ // to simplify user-land code. This is useful because we re-call the
4654
+ // `patchRoutesOnNavigation` function for matched routes with params.
4655
+ let uniqueChildren = children.filter(
4656
+ (newRoute) =>
4657
+ !childrenToPatch.some((existingRoute) =>
4658
+ isSameRoute(newRoute, existingRoute)
4659
+ )
4660
+ );
4661
+
4662
+ let newRoutes = convertRoutesToDataRoutes(
4663
+ uniqueChildren,
4664
+ mapRouteProperties,
4665
+ [routeId || "_", "patch", String(childrenToPatch?.length || "0")],
4666
+ manifest
4667
+ );
4668
+
4669
+ childrenToPatch.push(...newRoutes);
4670
+ }
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;
4689
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
+ );
4690
4710
  }
4691
4711
 
4692
4712
  /**
@@ -5038,7 +5058,7 @@ async function convertDataStrategyResultToDataResult(
5038
5058
  };
5039
5059
  }
5040
5060
 
5041
- // Convert thrown unstable_data() to ErrorResponse instances
5061
+ // Convert thrown data() to ErrorResponse instances
5042
5062
  result = new ErrorResponseImpl(
5043
5063
  result.init?.status || 500,
5044
5064
  undefined,
@@ -5310,7 +5330,6 @@ function processRouteLoaderData(
5310
5330
  function processLoaderData(
5311
5331
  state: RouterState,
5312
5332
  matches: AgnosticDataRouteMatch[],
5313
- matchesToLoad: AgnosticDataRouteMatch[],
5314
5333
  results: Record<string, DataResult>,
5315
5334
  pendingActionResult: PendingActionResult | undefined,
5316
5335
  revalidatingFetchers: RevalidatingFetcher[],
@@ -5477,7 +5496,7 @@ function getInternalRouterError(
5477
5496
  statusText = "Bad Request";
5478
5497
  if (type === "route-discovery") {
5479
5498
  errorMessage =
5480
- `Unable to match URL "${pathname}" - the \`unstable_patchRoutesOnNavigation()\` ` +
5499
+ `Unable to match URL "${pathname}" - the \`patchRoutesOnNavigation()\` ` +
5481
5500
  `function threw the following error:\n${message}`;
5482
5501
  } else if (method && pathname && routeId) {
5483
5502
  errorMessage =