@remix-run/router 1.19.1 → 1.19.2

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
@@ -22,7 +22,7 @@ import type {
22
22
  FormEncType,
23
23
  FormMethod,
24
24
  HTMLFormMethod,
25
- HandlerResult,
25
+ DataStrategyResult,
26
26
  ImmutableRouteKey,
27
27
  MapRoutePropertiesFunction,
28
28
  MutationFormMethod,
@@ -1033,7 +1033,7 @@ export function createRouter(init: RouterInit): Router {
1033
1033
 
1034
1034
  // Flag to ignore the next history update, so we can revert the URL change on
1035
1035
  // a POP navigation that was blocked by the user without touching router state
1036
- let ignoreNextHistoryUpdate = false;
1036
+ let unblockBlockerHistoryUpdate: (() => void) | undefined = undefined;
1037
1037
 
1038
1038
  // Initialize the router, all side effects should be kicked off from here.
1039
1039
  // Implemented as a Fluent API for ease of:
@@ -1045,8 +1045,9 @@ export function createRouter(init: RouterInit): Router {
1045
1045
  ({ action: historyAction, location, delta }) => {
1046
1046
  // Ignore this event if it was just us resetting the URL from a
1047
1047
  // blocked POP navigation
1048
- if (ignoreNextHistoryUpdate) {
1049
- ignoreNextHistoryUpdate = false;
1048
+ if (unblockBlockerHistoryUpdate) {
1049
+ unblockBlockerHistoryUpdate();
1050
+ unblockBlockerHistoryUpdate = undefined;
1050
1051
  return;
1051
1052
  }
1052
1053
 
@@ -1068,7 +1069,9 @@ export function createRouter(init: RouterInit): Router {
1068
1069
 
1069
1070
  if (blockerKey && delta != null) {
1070
1071
  // Restore the URL to match the current UI, but don't update router state
1071
- ignoreNextHistoryUpdate = true;
1072
+ let nextHistoryUpdatePromise = new Promise<void>((resolve) => {
1073
+ unblockBlockerHistoryUpdate = resolve;
1074
+ });
1072
1075
  init.history.go(delta * -1);
1073
1076
 
1074
1077
  // Put the blocker into a blocked state
@@ -1082,8 +1085,10 @@ export function createRouter(init: RouterInit): Router {
1082
1085
  reset: undefined,
1083
1086
  location,
1084
1087
  });
1085
- // Re-do the same POP navigation we just blocked
1086
- init.history.go(delta);
1088
+ // Re-do the same POP navigation we just blocked, after the url
1089
+ // restoration is also complete. See:
1090
+ // https://github.com/remix-run/react-router/issues/11613
1091
+ nextHistoryUpdatePromise.then(() => init.history.go(delta));
1087
1092
  },
1088
1093
  reset() {
1089
1094
  let blockers = new Map(state.blockers);
@@ -1479,7 +1484,11 @@ export function createRouter(init: RouterInit): Router {
1479
1484
  startNavigation(
1480
1485
  pendingAction || state.historyAction,
1481
1486
  state.navigation.location,
1482
- { overrideNavigation: state.navigation }
1487
+ {
1488
+ overrideNavigation: state.navigation,
1489
+ // Proxy through any rending view transition
1490
+ enableViewTransition: pendingViewTransitionEnabled === true,
1491
+ }
1483
1492
  );
1484
1493
  }
1485
1494
 
@@ -1749,11 +1758,13 @@ export function createRouter(init: RouterInit): Router {
1749
1758
  } else {
1750
1759
  let results = await callDataStrategy(
1751
1760
  "action",
1761
+ state,
1752
1762
  request,
1753
1763
  [actionMatch],
1754
- matches
1764
+ matches,
1765
+ null
1755
1766
  );
1756
- result = results[0];
1767
+ result = results[actionMatch.route.id];
1757
1768
 
1758
1769
  if (request.signal.aborted) {
1759
1770
  return { shortCircuited: true };
@@ -1775,7 +1786,7 @@ export function createRouter(init: RouterInit): Router {
1775
1786
  );
1776
1787
  replace = location === state.location.pathname + state.location.search;
1777
1788
  }
1778
- await startRedirectNavigation(request, result, {
1789
+ await startRedirectNavigation(request, result, true, {
1779
1790
  submission,
1780
1791
  replace,
1781
1792
  });
@@ -1995,7 +2006,7 @@ export function createRouter(init: RouterInit): Router {
1995
2006
 
1996
2007
  let { loaderResults, fetcherResults } =
1997
2008
  await callLoadersAndMaybeResolveData(
1998
- state.matches,
2009
+ state,
1999
2010
  matches,
2000
2011
  matchesToLoad,
2001
2012
  revalidatingFetchers,
@@ -2018,17 +2029,21 @@ export function createRouter(init: RouterInit): Router {
2018
2029
  revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
2019
2030
 
2020
2031
  // If any loaders returned a redirect Response, start a new REPLACE navigation
2021
- let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2032
+ let redirect = findRedirect(loaderResults);
2022
2033
  if (redirect) {
2023
- if (redirect.idx >= matchesToLoad.length) {
2024
- // If this redirect came from a fetcher make sure we mark it in
2025
- // fetchRedirectIds so it doesn't get revalidated on the next set of
2026
- // loader executions
2027
- let fetcherKey =
2028
- revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2029
- fetchRedirectIds.add(fetcherKey);
2030
- }
2031
- await startRedirectNavigation(request, redirect.result, {
2034
+ await startRedirectNavigation(request, redirect.result, true, {
2035
+ replace,
2036
+ });
2037
+ return { shortCircuited: true };
2038
+ }
2039
+
2040
+ redirect = findRedirect(fetcherResults);
2041
+ if (redirect) {
2042
+ // If this redirect came from a fetcher make sure we mark it in
2043
+ // fetchRedirectIds so it doesn't get revalidated on the next set of
2044
+ // loader executions
2045
+ fetchRedirectIds.add(redirect.key);
2046
+ await startRedirectNavigation(request, redirect.result, true, {
2032
2047
  replace,
2033
2048
  });
2034
2049
  return { shortCircuited: true };
@@ -2287,11 +2302,13 @@ export function createRouter(init: RouterInit): Router {
2287
2302
  let originatingLoadId = incrementingLoadId;
2288
2303
  let actionResults = await callDataStrategy(
2289
2304
  "action",
2305
+ state,
2290
2306
  fetchRequest,
2291
2307
  [match],
2292
- requestMatches
2308
+ requestMatches,
2309
+ key
2293
2310
  );
2294
- let actionResult = actionResults[0];
2311
+ let actionResult = actionResults[match.route.id];
2295
2312
 
2296
2313
  if (fetchRequest.signal.aborted) {
2297
2314
  // We can delete this so long as we weren't aborted by our own fetcher
@@ -2324,7 +2341,7 @@ export function createRouter(init: RouterInit): Router {
2324
2341
  } else {
2325
2342
  fetchRedirectIds.add(key);
2326
2343
  updateFetcherState(key, getLoadingFetcher(submission));
2327
- return startRedirectNavigation(fetchRequest, actionResult, {
2344
+ return startRedirectNavigation(fetchRequest, actionResult, false, {
2328
2345
  fetcherSubmission: submission,
2329
2346
  });
2330
2347
  }
@@ -2415,7 +2432,7 @@ export function createRouter(init: RouterInit): Router {
2415
2432
 
2416
2433
  let { loaderResults, fetcherResults } =
2417
2434
  await callLoadersAndMaybeResolveData(
2418
- state.matches,
2435
+ state,
2419
2436
  matches,
2420
2437
  matchesToLoad,
2421
2438
  revalidatingFetchers,
@@ -2435,23 +2452,32 @@ export function createRouter(init: RouterInit): Router {
2435
2452
  fetchControllers.delete(key);
2436
2453
  revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
2437
2454
 
2438
- let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2455
+ let redirect = findRedirect(loaderResults);
2439
2456
  if (redirect) {
2440
- if (redirect.idx >= matchesToLoad.length) {
2441
- // If this redirect came from a fetcher make sure we mark it in
2442
- // fetchRedirectIds so it doesn't get revalidated on the next set of
2443
- // loader executions
2444
- let fetcherKey =
2445
- revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2446
- fetchRedirectIds.add(fetcherKey);
2447
- }
2448
- return startRedirectNavigation(revalidationRequest, redirect.result);
2457
+ return startRedirectNavigation(
2458
+ revalidationRequest,
2459
+ redirect.result,
2460
+ false
2461
+ );
2462
+ }
2463
+
2464
+ redirect = findRedirect(fetcherResults);
2465
+ if (redirect) {
2466
+ // If this redirect came from a fetcher make sure we mark it in
2467
+ // fetchRedirectIds so it doesn't get revalidated on the next set of
2468
+ // loader executions
2469
+ fetchRedirectIds.add(redirect.key);
2470
+ return startRedirectNavigation(
2471
+ revalidationRequest,
2472
+ redirect.result,
2473
+ false
2474
+ );
2449
2475
  }
2450
2476
 
2451
2477
  // Process and commit output from loaders
2452
2478
  let { loaderData, errors } = processLoaderData(
2453
2479
  state,
2454
- state.matches,
2480
+ matches,
2455
2481
  matchesToLoad,
2456
2482
  loaderResults,
2457
2483
  undefined,
@@ -2564,11 +2590,13 @@ export function createRouter(init: RouterInit): Router {
2564
2590
  let originatingLoadId = incrementingLoadId;
2565
2591
  let results = await callDataStrategy(
2566
2592
  "loader",
2593
+ state,
2567
2594
  fetchRequest,
2568
2595
  [match],
2569
- matches
2596
+ matches,
2597
+ key
2570
2598
  );
2571
- let result = results[0];
2599
+ let result = results[match.route.id];
2572
2600
 
2573
2601
  // Deferred isn't supported for fetcher loads, await everything and treat it
2574
2602
  // as a normal load. resolveDeferredData will return undefined if this
@@ -2606,7 +2634,7 @@ export function createRouter(init: RouterInit): Router {
2606
2634
  return;
2607
2635
  } else {
2608
2636
  fetchRedirectIds.add(key);
2609
- await startRedirectNavigation(fetchRequest, result);
2637
+ await startRedirectNavigation(fetchRequest, result, false);
2610
2638
  return;
2611
2639
  }
2612
2640
  }
@@ -2645,6 +2673,7 @@ export function createRouter(init: RouterInit): Router {
2645
2673
  async function startRedirectNavigation(
2646
2674
  request: Request,
2647
2675
  redirect: RedirectResult,
2676
+ isNavigation: boolean,
2648
2677
  {
2649
2678
  submission,
2650
2679
  fetcherSubmission,
@@ -2731,8 +2760,11 @@ export function createRouter(init: RouterInit): Router {
2731
2760
  ...activeSubmission,
2732
2761
  formAction: location,
2733
2762
  },
2734
- // Preserve this flag across redirects
2763
+ // Preserve these flags across redirects
2735
2764
  preventScrollReset: pendingPreventScrollReset,
2765
+ enableViewTransition: isNavigation
2766
+ ? pendingViewTransitionEnabled
2767
+ : undefined,
2736
2768
  });
2737
2769
  } else {
2738
2770
  // If we have a navigation submission, we will preserve it through the
@@ -2745,8 +2777,11 @@ export function createRouter(init: RouterInit): Router {
2745
2777
  overrideNavigation,
2746
2778
  // Send fetcher submissions through for shouldRevalidate
2747
2779
  fetcherSubmission,
2748
- // Preserve this flag across redirects
2780
+ // Preserve these flags across redirects
2749
2781
  preventScrollReset: pendingPreventScrollReset,
2782
+ enableViewTransition: isNavigation
2783
+ ? pendingViewTransitionEnabled
2784
+ : undefined,
2750
2785
  });
2751
2786
  }
2752
2787
  }
@@ -2755,102 +2790,123 @@ export function createRouter(init: RouterInit): Router {
2755
2790
  // pass around the manifest, mapRouteProperties, etc.
2756
2791
  async function callDataStrategy(
2757
2792
  type: "loader" | "action",
2793
+ state: RouterState,
2758
2794
  request: Request,
2759
2795
  matchesToLoad: AgnosticDataRouteMatch[],
2760
- matches: AgnosticDataRouteMatch[]
2761
- ): Promise<DataResult[]> {
2796
+ matches: AgnosticDataRouteMatch[],
2797
+ fetcherKey: string | null
2798
+ ): Promise<Record<string, DataResult>> {
2799
+ let results: Record<string, DataStrategyResult>;
2800
+ let dataResults: Record<string, DataResult> = {};
2762
2801
  try {
2763
- let results = await callDataStrategyImpl(
2802
+ results = await callDataStrategyImpl(
2764
2803
  dataStrategyImpl,
2765
2804
  type,
2805
+ state,
2766
2806
  request,
2767
2807
  matchesToLoad,
2768
2808
  matches,
2809
+ fetcherKey,
2769
2810
  manifest,
2770
2811
  mapRouteProperties
2771
2812
  );
2772
-
2773
- return await Promise.all(
2774
- results.map((result, i) => {
2775
- if (isRedirectHandlerResult(result)) {
2776
- let response = result.result as Response;
2777
- return {
2778
- type: ResultType.redirect,
2779
- response: normalizeRelativeRoutingRedirectResponse(
2780
- response,
2781
- request,
2782
- matchesToLoad[i].route.id,
2783
- matches,
2784
- basename,
2785
- future.v7_relativeSplatPath
2786
- ),
2787
- };
2788
- }
2789
-
2790
- return convertHandlerResultToDataResult(result);
2791
- })
2792
- );
2793
2813
  } catch (e) {
2794
2814
  // If the outer dataStrategy method throws, just return the error for all
2795
2815
  // matches - and it'll naturally bubble to the root
2796
- return matchesToLoad.map(() => ({
2797
- type: ResultType.error,
2798
- error: e,
2799
- }));
2816
+ matchesToLoad.forEach((m) => {
2817
+ dataResults[m.route.id] = {
2818
+ type: ResultType.error,
2819
+ error: e,
2820
+ };
2821
+ });
2822
+ return dataResults;
2823
+ }
2824
+
2825
+ for (let [routeId, result] of Object.entries(results)) {
2826
+ if (isRedirectDataStrategyResultResult(result)) {
2827
+ let response = result.result as Response;
2828
+ dataResults[routeId] = {
2829
+ type: ResultType.redirect,
2830
+ response: normalizeRelativeRoutingRedirectResponse(
2831
+ response,
2832
+ request,
2833
+ routeId,
2834
+ matches,
2835
+ basename,
2836
+ future.v7_relativeSplatPath
2837
+ ),
2838
+ };
2839
+ } else {
2840
+ dataResults[routeId] = await convertDataStrategyResultToDataResult(
2841
+ result
2842
+ );
2843
+ }
2800
2844
  }
2845
+
2846
+ return dataResults;
2801
2847
  }
2802
2848
 
2803
2849
  async function callLoadersAndMaybeResolveData(
2804
- currentMatches: AgnosticDataRouteMatch[],
2850
+ state: RouterState,
2805
2851
  matches: AgnosticDataRouteMatch[],
2806
2852
  matchesToLoad: AgnosticDataRouteMatch[],
2807
2853
  fetchersToLoad: RevalidatingFetcher[],
2808
2854
  request: Request
2809
2855
  ) {
2810
- let [loaderResults, ...fetcherResults] = await Promise.all([
2811
- matchesToLoad.length
2812
- ? callDataStrategy("loader", request, matchesToLoad, matches)
2813
- : [],
2814
- ...fetchersToLoad.map((f) => {
2856
+ let currentMatches = state.matches;
2857
+
2858
+ // Kick off loaders and fetchers in parallel
2859
+ let loaderResultsPromise = callDataStrategy(
2860
+ "loader",
2861
+ state,
2862
+ request,
2863
+ matchesToLoad,
2864
+ matches,
2865
+ null
2866
+ );
2867
+
2868
+ let fetcherResultsPromise = Promise.all(
2869
+ fetchersToLoad.map(async (f) => {
2815
2870
  if (f.matches && f.match && f.controller) {
2816
- let fetcherRequest = createClientSideRequest(
2817
- init.history,
2818
- f.path,
2819
- f.controller.signal
2820
- );
2821
- return callDataStrategy(
2871
+ let results = await callDataStrategy(
2822
2872
  "loader",
2823
- fetcherRequest,
2873
+ state,
2874
+ createClientSideRequest(init.history, f.path, f.controller.signal),
2824
2875
  [f.match],
2825
- f.matches
2826
- ).then((r) => r[0]);
2876
+ f.matches,
2877
+ f.key
2878
+ );
2879
+ let result = results[f.match.route.id];
2880
+ // Fetcher results are keyed by fetcher key from here on out, not routeId
2881
+ return { [f.key]: result };
2827
2882
  } else {
2828
- return Promise.resolve<DataResult>({
2829
- type: ResultType.error,
2830
- error: getInternalRouterError(404, {
2831
- pathname: f.path,
2832
- }),
2883
+ return Promise.resolve({
2884
+ [f.key]: {
2885
+ type: ResultType.error,
2886
+ error: getInternalRouterError(404, {
2887
+ pathname: f.path,
2888
+ }),
2889
+ } as ErrorResult,
2833
2890
  });
2834
2891
  }
2835
- }),
2836
- ]);
2892
+ })
2893
+ );
2894
+
2895
+ let loaderResults = await loaderResultsPromise;
2896
+ let fetcherResults = (await fetcherResultsPromise).reduce(
2897
+ (acc, r) => Object.assign(acc, r),
2898
+ {}
2899
+ );
2837
2900
 
2838
2901
  await Promise.all([
2839
- resolveDeferredResults(
2840
- currentMatches,
2841
- matchesToLoad,
2902
+ resolveNavigationDeferredResults(
2903
+ matches,
2842
2904
  loaderResults,
2843
- loaderResults.map(() => request.signal),
2844
- false,
2845
- state.loaderData
2846
- ),
2847
- resolveDeferredResults(
2905
+ request.signal,
2848
2906
  currentMatches,
2849
- fetchersToLoad.map((f) => f.match),
2850
- fetcherResults,
2851
- fetchersToLoad.map((f) => (f.controller ? f.controller.signal : null)),
2852
- true
2907
+ state.loaderData
2853
2908
  ),
2909
+ resolveFetcherDeferredResults(matches, fetcherResults, fetchersToLoad),
2854
2910
  ]);
2855
2911
 
2856
2912
  return {
@@ -3700,9 +3756,9 @@ export function createStaticHandler(
3700
3756
  };
3701
3757
  } catch (e) {
3702
3758
  // If the user threw/returned a Response in callLoaderOrAction for a
3703
- // `queryRoute` call, we throw the `HandlerResult` to bail out early
3759
+ // `queryRoute` call, we throw the `DataStrategyResult` to bail out early
3704
3760
  // and then return or throw the raw Response here accordingly
3705
- if (isHandlerResult(e) && isResponse(e.result)) {
3761
+ if (isDataStrategyResult(e) && isResponse(e.result)) {
3706
3762
  if (e.type === ResultType.error) {
3707
3763
  throw e.result;
3708
3764
  }
@@ -3751,7 +3807,7 @@ export function createStaticHandler(
3751
3807
  requestContext,
3752
3808
  unstable_dataStrategy
3753
3809
  );
3754
- result = results[0];
3810
+ result = results[actionMatch.route.id];
3755
3811
 
3756
3812
  if (request.signal.aborted) {
3757
3813
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
@@ -3942,7 +3998,6 @@ export function createStaticHandler(
3942
3998
  let activeDeferreds = new Map<string, DeferredData>();
3943
3999
  let context = processRouteLoaderData(
3944
4000
  matches,
3945
- matchesToLoad,
3946
4001
  results,
3947
4002
  pendingActionResult,
3948
4003
  activeDeferreds,
@@ -3979,27 +4034,34 @@ export function createStaticHandler(
3979
4034
  isRouteRequest: boolean,
3980
4035
  requestContext: unknown,
3981
4036
  unstable_dataStrategy: DataStrategyFunction | null
3982
- ): Promise<DataResult[]> {
4037
+ ): Promise<Record<string, DataResult>> {
3983
4038
  let results = await callDataStrategyImpl(
3984
4039
  unstable_dataStrategy || defaultDataStrategy,
3985
4040
  type,
4041
+ null,
3986
4042
  request,
3987
4043
  matchesToLoad,
3988
4044
  matches,
4045
+ null,
3989
4046
  manifest,
3990
4047
  mapRouteProperties,
3991
4048
  requestContext
3992
4049
  );
3993
4050
 
3994
- return await Promise.all(
3995
- results.map((result, i) => {
3996
- if (isRedirectHandlerResult(result)) {
4051
+ let dataResults: Record<string, DataResult> = {};
4052
+ await Promise.all(
4053
+ matches.map(async (match) => {
4054
+ if (!(match.route.id in results)) {
4055
+ return;
4056
+ }
4057
+ let result = results[match.route.id];
4058
+ if (isRedirectDataStrategyResultResult(result)) {
3997
4059
  let response = result.result as Response;
3998
4060
  // Throw redirects and let the server handle them with an HTTP redirect
3999
4061
  throw normalizeRelativeRoutingRedirectResponse(
4000
4062
  response,
4001
4063
  request,
4002
- matchesToLoad[i].route.id,
4064
+ match.route.id,
4003
4065
  matches,
4004
4066
  basename,
4005
4067
  future.v7_relativeSplatPath
@@ -4011,9 +4073,11 @@ export function createStaticHandler(
4011
4073
  throw result;
4012
4074
  }
4013
4075
 
4014
- return convertHandlerResultToDataResult(result);
4076
+ dataResults[match.route.id] =
4077
+ await convertDataStrategyResultToDataResult(result);
4015
4078
  })
4016
4079
  );
4080
+ return dataResults;
4017
4081
  }
4018
4082
 
4019
4083
  return {
@@ -4704,77 +4768,91 @@ async function loadLazyRouteModule(
4704
4768
  }
4705
4769
 
4706
4770
  // Default implementation of `dataStrategy` which fetches all loaders in parallel
4707
- function defaultDataStrategy(
4708
- opts: DataStrategyFunctionArgs
4709
- ): ReturnType<DataStrategyFunction> {
4710
- return Promise.all(opts.matches.map((m) => m.resolve()));
4771
+ async function defaultDataStrategy({
4772
+ matches,
4773
+ }: DataStrategyFunctionArgs): ReturnType<DataStrategyFunction> {
4774
+ let matchesToLoad = matches.filter((m) => m.shouldLoad);
4775
+ let results = await Promise.all(matchesToLoad.map((m) => m.resolve()));
4776
+ return results.reduce(
4777
+ (acc, result, i) =>
4778
+ Object.assign(acc, { [matchesToLoad[i].route.id]: result }),
4779
+ {}
4780
+ );
4711
4781
  }
4712
4782
 
4713
4783
  async function callDataStrategyImpl(
4714
4784
  dataStrategyImpl: DataStrategyFunction,
4715
4785
  type: "loader" | "action",
4786
+ state: RouterState | null,
4716
4787
  request: Request,
4717
4788
  matchesToLoad: AgnosticDataRouteMatch[],
4718
4789
  matches: AgnosticDataRouteMatch[],
4790
+ fetcherKey: string | null,
4719
4791
  manifest: RouteManifest,
4720
4792
  mapRouteProperties: MapRoutePropertiesFunction,
4721
4793
  requestContext?: unknown
4722
- ): Promise<HandlerResult[]> {
4723
- let routeIdsToLoad = matchesToLoad.reduce(
4724
- (acc, m) => acc.add(m.route.id),
4725
- new Set<string>()
4794
+ ): Promise<Record<string, DataStrategyResult>> {
4795
+ let loadRouteDefinitionsPromises = matches.map((m) =>
4796
+ m.route.lazy
4797
+ ? loadLazyRouteModule(m.route, mapRouteProperties, manifest)
4798
+ : undefined
4726
4799
  );
4727
- let loadedMatches = new Set<string>();
4800
+
4801
+ let dsMatches = matches.map((match, i) => {
4802
+ let loadRoutePromise = loadRouteDefinitionsPromises[i];
4803
+ let shouldLoad = matchesToLoad.some((m) => m.route.id === match.route.id);
4804
+ // `resolve` encapsulates route.lazy(), executing the loader/action,
4805
+ // and mapping return values/thrown errors to a `DataStrategyResult`. Users
4806
+ // can pass a callback to take fine-grained control over the execution
4807
+ // of the loader/action
4808
+ let resolve: DataStrategyMatch["resolve"] = async (handlerOverride) => {
4809
+ if (
4810
+ handlerOverride &&
4811
+ request.method === "GET" &&
4812
+ (match.route.lazy || match.route.loader)
4813
+ ) {
4814
+ shouldLoad = true;
4815
+ }
4816
+ return shouldLoad
4817
+ ? callLoaderOrAction(
4818
+ type,
4819
+ request,
4820
+ match,
4821
+ loadRoutePromise,
4822
+ handlerOverride,
4823
+ requestContext
4824
+ )
4825
+ : Promise.resolve({ type: ResultType.data, result: undefined });
4826
+ };
4827
+
4828
+ return {
4829
+ ...match,
4830
+ shouldLoad,
4831
+ resolve,
4832
+ };
4833
+ });
4728
4834
 
4729
4835
  // Send all matches here to allow for a middleware-type implementation.
4730
4836
  // handler will be a no-op for unneeded routes and we filter those results
4731
4837
  // back out below.
4732
4838
  let results = await dataStrategyImpl({
4733
- matches: matches.map((match) => {
4734
- let shouldLoad = routeIdsToLoad.has(match.route.id);
4735
- // `resolve` encapsulates the route.lazy, executing the
4736
- // loader/action, and mapping return values/thrown errors to a
4737
- // HandlerResult. Users can pass a callback to take fine-grained control
4738
- // over the execution of the loader/action
4739
- let resolve: DataStrategyMatch["resolve"] = (handlerOverride) => {
4740
- loadedMatches.add(match.route.id);
4741
- return shouldLoad
4742
- ? callLoaderOrAction(
4743
- type,
4744
- request,
4745
- match,
4746
- manifest,
4747
- mapRouteProperties,
4748
- handlerOverride,
4749
- requestContext
4750
- )
4751
- : Promise.resolve({ type: ResultType.data, result: undefined });
4752
- };
4753
-
4754
- return {
4755
- ...match,
4756
- shouldLoad,
4757
- resolve,
4758
- };
4759
- }),
4839
+ matches: dsMatches,
4760
4840
  request,
4761
4841
  params: matches[0].params,
4842
+ fetcherKey,
4762
4843
  context: requestContext,
4763
4844
  });
4764
4845
 
4765
- // Throw if any loadRoute implementations not called since they are what
4766
- // ensures a route is fully loaded
4767
- matches.forEach((m) =>
4768
- invariant(
4769
- loadedMatches.has(m.route.id),
4770
- `\`match.resolve()\` was not called for route id "${m.route.id}". ` +
4771
- "You must call `match.resolve()` on every match passed to " +
4772
- "`dataStrategy` to ensure all routes are properly loaded."
4773
- )
4774
- );
4846
+ // Wait for all routes to load here but 'swallow the error since we want
4847
+ // it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` -
4848
+ // called from `match.resolve()`
4849
+ try {
4850
+ await Promise.all(loadRouteDefinitionsPromises);
4851
+ } catch (e) {
4852
+ // No-op
4853
+ }
4775
4854
 
4776
- // Filter out any middleware-only matches for which we didn't need to run handlers
4777
- return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
4855
+ return results;
4778
4856
  }
4779
4857
 
4780
4858
  // Default logic for calling a loader/action is the user has no specified a dataStrategy
@@ -4782,22 +4860,21 @@ async function callLoaderOrAction(
4782
4860
  type: "loader" | "action",
4783
4861
  request: Request,
4784
4862
  match: AgnosticDataRouteMatch,
4785
- manifest: RouteManifest,
4786
- mapRouteProperties: MapRoutePropertiesFunction,
4863
+ loadRoutePromise: Promise<void> | undefined,
4787
4864
  handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
4788
4865
  staticContext?: unknown
4789
- ): Promise<HandlerResult> {
4790
- let result: HandlerResult;
4866
+ ): Promise<DataStrategyResult> {
4867
+ let result: DataStrategyResult;
4791
4868
  let onReject: (() => void) | undefined;
4792
4869
 
4793
4870
  let runHandler = (
4794
4871
  handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
4795
- ): Promise<HandlerResult> => {
4872
+ ): Promise<DataStrategyResult> => {
4796
4873
  // Setup a promise we can race against so that abort signals short circuit
4797
4874
  let reject: () => void;
4798
- // This will never resolve so safe to type it as Promise<HandlerResult> to
4875
+ // This will never resolve so safe to type it as Promise<DataStrategyResult> to
4799
4876
  // satisfy the function return value
4800
- let abortPromise = new Promise<HandlerResult>((_, r) => (reject = r));
4877
+ let abortPromise = new Promise<DataStrategyResult>((_, r) => (reject = r));
4801
4878
  onReject = () => reject();
4802
4879
  request.signal.addEventListener("abort", onReject);
4803
4880
 
@@ -4820,19 +4897,16 @@ async function callLoaderOrAction(
4820
4897
  );
4821
4898
  };
4822
4899
 
4823
- let handlerPromise: Promise<HandlerResult>;
4824
- if (handlerOverride) {
4825
- handlerPromise = handlerOverride((ctx: unknown) => actualHandler(ctx));
4826
- } else {
4827
- handlerPromise = (async () => {
4828
- try {
4829
- let val = await actualHandler();
4830
- return { type: "data", result: val };
4831
- } catch (e) {
4832
- return { type: "error", result: e };
4833
- }
4834
- })();
4835
- }
4900
+ let handlerPromise: Promise<DataStrategyResult> = (async () => {
4901
+ try {
4902
+ let val = await (handlerOverride
4903
+ ? handlerOverride((ctx: unknown) => actualHandler(ctx))
4904
+ : actualHandler());
4905
+ return { type: "data", result: val };
4906
+ } catch (e) {
4907
+ return { type: "error", result: e };
4908
+ }
4909
+ })();
4836
4910
 
4837
4911
  return Promise.race([handlerPromise, abortPromise]);
4838
4912
  };
@@ -4840,7 +4914,8 @@ async function callLoaderOrAction(
4840
4914
  try {
4841
4915
  let handler = match.route[type];
4842
4916
 
4843
- if (match.route.lazy) {
4917
+ // If we have a route.lazy promise, await that first
4918
+ if (loadRoutePromise) {
4844
4919
  if (handler) {
4845
4920
  // Run statically defined handler in parallel with lazy()
4846
4921
  let handlerError;
@@ -4851,7 +4926,7 @@ async function callLoaderOrAction(
4851
4926
  runHandler(handler).catch((e) => {
4852
4927
  handlerError = e;
4853
4928
  }),
4854
- loadLazyRouteModule(match.route, mapRouteProperties, manifest),
4929
+ loadRoutePromise,
4855
4930
  ]);
4856
4931
  if (handlerError !== undefined) {
4857
4932
  throw handlerError;
@@ -4859,7 +4934,7 @@ async function callLoaderOrAction(
4859
4934
  result = value!;
4860
4935
  } else {
4861
4936
  // Load lazy route module, then run any returned handler
4862
- await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4937
+ await loadRoutePromise;
4863
4938
 
4864
4939
  handler = match.route[type];
4865
4940
  if (handler) {
@@ -4899,7 +4974,7 @@ async function callLoaderOrAction(
4899
4974
  );
4900
4975
  } catch (e) {
4901
4976
  // We should already be catching and converting normal handler executions to
4902
- // HandlerResults and returning them, so anything that throws here is an
4977
+ // DataStrategyResults and returning them, so anything that throws here is an
4903
4978
  // unexpected error we still need to wrap
4904
4979
  return { type: ResultType.error, result: e };
4905
4980
  } finally {
@@ -4911,10 +4986,10 @@ async function callLoaderOrAction(
4911
4986
  return result;
4912
4987
  }
4913
4988
 
4914
- async function convertHandlerResultToDataResult(
4915
- handlerResult: HandlerResult
4989
+ async function convertDataStrategyResultToDataResult(
4990
+ dataStrategyResult: DataStrategyResult
4916
4991
  ): Promise<DataResult> {
4917
- let { result, type } = handlerResult;
4992
+ let { result, type } = dataStrategyResult;
4918
4993
 
4919
4994
  if (isResponse(result)) {
4920
4995
  let data: any;
@@ -5116,8 +5191,7 @@ function convertSearchParamsToFormData(
5116
5191
 
5117
5192
  function processRouteLoaderData(
5118
5193
  matches: AgnosticDataRouteMatch[],
5119
- matchesToLoad: AgnosticDataRouteMatch[],
5120
- results: DataResult[],
5194
+ results: Record<string, DataResult>,
5121
5195
  pendingActionResult: PendingActionResult | undefined,
5122
5196
  activeDeferreds: Map<string, DeferredData>,
5123
5197
  skipLoaderErrorBubbling: boolean
@@ -5139,8 +5213,12 @@ function processRouteLoaderData(
5139
5213
  : undefined;
5140
5214
 
5141
5215
  // Process loader results into state.loaderData/state.errors
5142
- results.forEach((result, index) => {
5143
- let id = matchesToLoad[index].route.id;
5216
+ matches.forEach((match) => {
5217
+ if (!(match.route.id in results)) {
5218
+ return;
5219
+ }
5220
+ let id = match.route.id;
5221
+ let result = results[id];
5144
5222
  invariant(
5145
5223
  !isRedirectResult(result),
5146
5224
  "Cannot handle redirect results in processLoaderData"
@@ -5233,10 +5311,10 @@ function processLoaderData(
5233
5311
  state: RouterState,
5234
5312
  matches: AgnosticDataRouteMatch[],
5235
5313
  matchesToLoad: AgnosticDataRouteMatch[],
5236
- results: DataResult[],
5314
+ results: Record<string, DataResult>,
5237
5315
  pendingActionResult: PendingActionResult | undefined,
5238
5316
  revalidatingFetchers: RevalidatingFetcher[],
5239
- fetcherResults: DataResult[],
5317
+ fetcherResults: Record<string, DataResult>,
5240
5318
  activeDeferreds: Map<string, DeferredData>
5241
5319
  ): {
5242
5320
  loaderData: RouterState["loaderData"];
@@ -5244,7 +5322,6 @@ function processLoaderData(
5244
5322
  } {
5245
5323
  let { loaderData, errors } = processRouteLoaderData(
5246
5324
  matches,
5247
- matchesToLoad,
5248
5325
  results,
5249
5326
  pendingActionResult,
5250
5327
  activeDeferreds,
@@ -5252,18 +5329,15 @@ function processLoaderData(
5252
5329
  );
5253
5330
 
5254
5331
  // Process results from our revalidating fetchers
5255
- for (let index = 0; index < revalidatingFetchers.length; index++) {
5256
- let { key, match, controller } = revalidatingFetchers[index];
5257
- invariant(
5258
- fetcherResults !== undefined && fetcherResults[index] !== undefined,
5259
- "Did not find corresponding fetcher result"
5260
- );
5261
- let result = fetcherResults[index];
5332
+ revalidatingFetchers.forEach((rf) => {
5333
+ let { key, match, controller } = rf;
5334
+ let result = fetcherResults[key];
5335
+ invariant(result, "Did not find corresponding fetcher result");
5262
5336
 
5263
5337
  // Process fetcher non-redirect errors
5264
5338
  if (controller && controller.signal.aborted) {
5265
5339
  // Nothing to do for aborted fetchers
5266
- continue;
5340
+ return;
5267
5341
  } else if (isErrorResult(result)) {
5268
5342
  let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
5269
5343
  if (!(errors && errors[boundaryMatch.route.id])) {
@@ -5285,7 +5359,7 @@ function processLoaderData(
5285
5359
  let doneFetcher = getDoneFetcher(result.data);
5286
5360
  state.fetchers.set(key, doneFetcher);
5287
5361
  }
5288
- }
5362
+ });
5289
5363
 
5290
5364
  return { loaderData, errors };
5291
5365
  }
@@ -5443,12 +5517,13 @@ function getInternalRouterError(
5443
5517
 
5444
5518
  // Find any returned redirect errors, starting from the lowest match
5445
5519
  function findRedirect(
5446
- results: DataResult[]
5447
- ): { result: RedirectResult; idx: number } | undefined {
5448
- for (let i = results.length - 1; i >= 0; i--) {
5449
- let result = results[i];
5520
+ results: Record<string, DataResult>
5521
+ ): { key: string; result: RedirectResult } | undefined {
5522
+ let entries = Object.entries(results);
5523
+ for (let i = entries.length - 1; i >= 0; i--) {
5524
+ let [key, result] = entries[i];
5450
5525
  if (isRedirectResult(result)) {
5451
- return { result, idx: i };
5526
+ return { key, result };
5452
5527
  }
5453
5528
  }
5454
5529
  }
@@ -5483,7 +5558,7 @@ function isPromise<T = unknown>(val: unknown): val is Promise<T> {
5483
5558
  return typeof val === "object" && val != null && "then" in val;
5484
5559
  }
5485
5560
 
5486
- function isHandlerResult(result: unknown): result is HandlerResult {
5561
+ function isDataStrategyResult(result: unknown): result is DataStrategyResult {
5487
5562
  return (
5488
5563
  result != null &&
5489
5564
  typeof result === "object" &&
@@ -5493,7 +5568,7 @@ function isHandlerResult(result: unknown): result is HandlerResult {
5493
5568
  );
5494
5569
  }
5495
5570
 
5496
- function isRedirectHandlerResult(result: HandlerResult) {
5571
+ function isRedirectDataStrategyResultResult(result: DataStrategyResult) {
5497
5572
  return (
5498
5573
  isResponse(result.result) && redirectStatusCodes.has(result.result.status)
5499
5574
  );
@@ -5566,17 +5641,17 @@ function isMutationMethod(
5566
5641
  return validMutationMethods.has(method.toLowerCase() as MutationFormMethod);
5567
5642
  }
5568
5643
 
5569
- async function resolveDeferredResults(
5644
+ async function resolveNavigationDeferredResults(
5645
+ matches: (AgnosticDataRouteMatch | null)[],
5646
+ results: Record<string, DataResult>,
5647
+ signal: AbortSignal,
5570
5648
  currentMatches: AgnosticDataRouteMatch[],
5571
- matchesToLoad: (AgnosticDataRouteMatch | null)[],
5572
- results: DataResult[],
5573
- signals: (AbortSignal | null)[],
5574
- isFetcher: boolean,
5575
- currentLoaderData?: RouteData
5649
+ currentLoaderData: RouteData
5576
5650
  ) {
5577
- for (let index = 0; index < results.length; index++) {
5578
- let result = results[index];
5579
- let match = matchesToLoad[index];
5651
+ let entries = Object.entries(results);
5652
+ for (let index = 0; index < entries.length; index++) {
5653
+ let [routeId, result] = entries[index];
5654
+ let match = matches.find((m) => m?.route.id === routeId);
5580
5655
  // If we don't have a match, then we can have a deferred result to do
5581
5656
  // anything with. This is for revalidating fetchers where the route was
5582
5657
  // removed during HMR
@@ -5592,24 +5667,54 @@ async function resolveDeferredResults(
5592
5667
  !isNewRouteInstance(currentMatch, match) &&
5593
5668
  (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
5594
5669
 
5595
- if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) {
5670
+ if (isDeferredResult(result) && isRevalidatingLoader) {
5596
5671
  // Note: we do not have to touch activeDeferreds here since we race them
5597
5672
  // against the signal in resolveDeferredData and they'll get aborted
5598
5673
  // there if needed
5599
- let signal = signals[index];
5600
- invariant(
5601
- signal,
5602
- "Expected an AbortSignal for revalidating fetcher deferred result"
5603
- );
5604
- await resolveDeferredData(result, signal, isFetcher).then((result) => {
5674
+ await resolveDeferredData(result, signal, false).then((result) => {
5605
5675
  if (result) {
5606
- results[index] = result || results[index];
5676
+ results[routeId] = result;
5607
5677
  }
5608
5678
  });
5609
5679
  }
5610
5680
  }
5611
5681
  }
5612
5682
 
5683
+ async function resolveFetcherDeferredResults(
5684
+ matches: (AgnosticDataRouteMatch | null)[],
5685
+ results: Record<string, DataResult>,
5686
+ revalidatingFetchers: RevalidatingFetcher[]
5687
+ ) {
5688
+ for (let index = 0; index < revalidatingFetchers.length; index++) {
5689
+ let { key, routeId, controller } = revalidatingFetchers[index];
5690
+ let result = results[key];
5691
+ let match = matches.find((m) => m?.route.id === routeId);
5692
+ // If we don't have a match, then we can have a deferred result to do
5693
+ // anything with. This is for revalidating fetchers where the route was
5694
+ // removed during HMR
5695
+ if (!match) {
5696
+ continue;
5697
+ }
5698
+
5699
+ if (isDeferredResult(result)) {
5700
+ // Note: we do not have to touch activeDeferreds here since we race them
5701
+ // against the signal in resolveDeferredData and they'll get aborted
5702
+ // there if needed
5703
+ invariant(
5704
+ controller,
5705
+ "Expected an AbortController for revalidating fetcher deferred result"
5706
+ );
5707
+ await resolveDeferredData(result, controller.signal, true).then(
5708
+ (result) => {
5709
+ if (result) {
5710
+ results[key] = result;
5711
+ }
5712
+ }
5713
+ );
5714
+ }
5715
+ }
5716
+ }
5717
+
5613
5718
  async function resolveDeferredData(
5614
5719
  result: DeferredResult,
5615
5720
  signal: AbortSignal,