Index: imaps-frontend/node_modules/@remix-run/router/router.ts
===================================================================
--- imaps-frontend/node_modules/@remix-run/router/router.ts	(revision d56544918790c0c3bc50f2f0a98c99f3d2988ccf)
+++ imaps-frontend/node_modules/@remix-run/router/router.ts	(revision 0c6b92add4cfaedcb91bcd37b45849f142f7d152)
@@ -23,5 +23,5 @@
   FormMethod,
   HTMLFormMethod,
-  HandlerResult,
+  DataStrategyResult,
   ImmutableRouteKey,
   MapRoutePropertiesFunction,
@@ -36,5 +36,5 @@
   V7_FormMethod,
   V7_MutationFormMethod,
-  AgnosticPatchRoutesOnMissFunction,
+  AgnosticPatchRoutesOnNavigationFunction,
   DataWithResponseInit,
 } from "./utils";
@@ -392,6 +392,6 @@
   hydrationData?: HydrationState;
   window?: Window;
-  unstable_patchRoutesOnMiss?: AgnosticPatchRoutesOnMissFunction;
-  unstable_dataStrategy?: DataStrategyFunction;
+  dataStrategy?: DataStrategyFunction;
+  patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction;
 }
 
@@ -423,5 +423,5 @@
       requestContext?: unknown;
       skipLoaderErrorBubbling?: boolean;
-      unstable_dataStrategy?: DataStrategyFunction;
+      dataStrategy?: DataStrategyFunction;
     }
   ): Promise<StaticHandlerContext | Response>;
@@ -431,5 +431,5 @@
       routeId?: string;
       requestContext?: unknown;
-      unstable_dataStrategy?: DataStrategyFunction;
+      dataStrategy?: DataStrategyFunction;
     }
   ): Promise<any>;
@@ -449,6 +449,6 @@
     opts: {
       deletedFetchers: string[];
-      unstable_viewTransitionOpts?: ViewTransitionOpts;
-      unstable_flushSync: boolean;
+      viewTransitionOpts?: ViewTransitionOpts;
+      flushSync: boolean;
     }
   ): void;
@@ -476,5 +476,5 @@
   preventScrollReset?: boolean;
   relative?: RelativeRoutingType;
-  unstable_flushSync?: boolean;
+  flushSync?: boolean;
 };
 
@@ -484,5 +484,5 @@
   state?: any;
   fromRouteId?: string;
-  unstable_viewTransition?: boolean;
+  viewTransition?: boolean;
 };
 
@@ -798,6 +798,6 @@
   let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
   let basename = init.basename || "/";
-  let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
-  let patchRoutesOnMissImpl = init.unstable_patchRoutesOnMiss;
+  let dataStrategyImpl = init.dataStrategy || defaultDataStrategy;
+  let patchRoutesOnNavigationImpl = init.patchRoutesOnNavigation;
 
   // Config driven behavior flags
@@ -832,5 +832,5 @@
   let initialErrors: RouteData | null = null;
 
-  if (initialMatches == null && !patchRoutesOnMissImpl) {
+  if (initialMatches == null && !patchRoutesOnNavigationImpl) {
     // If we do not match a user-provided-route, fall back to the root
     // to allow the error boundary to take over
@@ -843,5 +843,5 @@
   }
 
-  // In SPA apps, if the user provided a patchRoutesOnMiss implementation and
+  // In SPA apps, if the user provided a patchRoutesOnNavigation implementation and
   // our initial match is a splat route, clear them out so we run through lazy
   // discovery on hydration in case there's a more accurate lazy route match.
@@ -866,5 +866,5 @@
 
     // If partial hydration and fog of war is enabled, we will be running
-    // `patchRoutesOnMiss` during hydration so include any partial matches as
+    // `patchRoutesOnNavigation` during hydration so include any partial matches as
     // the initial matches so we can properly render `HydrateFallback`'s
     if (future.v7_partialHydration) {
@@ -891,23 +891,4 @@
     let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
     let errors = init.hydrationData ? init.hydrationData.errors : null;
-    let isRouteInitialized = (m: AgnosticDataRouteMatch) => {
-      // No loader, nothing to initialize
-      if (!m.route.loader) {
-        return true;
-      }
-      // Explicitly opting-in to running on hydration
-      if (
-        typeof m.route.loader === "function" &&
-        m.route.loader.hydrate === true
-      ) {
-        return false;
-      }
-      // Otherwise, initialized if hydrated with data or an error
-      return (
-        (loaderData && loaderData[m.route.id] !== undefined) ||
-        (errors && errors[m.route.id] !== undefined)
-      );
-    };
-
     // If errors exist, don't consider routes below the boundary
     if (errors) {
@@ -915,7 +896,11 @@
         (m) => errors![m.route.id] !== undefined
       );
-      initialized = initialMatches.slice(0, idx + 1).every(isRouteInitialized);
+      initialized = initialMatches
+        .slice(0, idx + 1)
+        .every((m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors));
     } else {
-      initialized = initialMatches.every(isRouteInitialized);
+      initialized = initialMatches.every(
+        (m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors)
+      );
     }
   } else {
@@ -1021,14 +1006,14 @@
   let blockerFunctions = new Map<string, BlockerFunction>();
 
-  // Map of pending patchRoutesOnMiss() promises (keyed by path/matches) so
+  // Map of pending patchRoutesOnNavigation() promises (keyed by path/matches) so
   // that we only kick them off once for a given combo
   let pendingPatchRoutes = new Map<
     string,
-    ReturnType<AgnosticPatchRoutesOnMissFunction>
+    ReturnType<AgnosticPatchRoutesOnNavigationFunction>
   >();
 
   // Flag to ignore the next history update, so we can revert the URL change on
   // a POP navigation that was blocked by the user without touching router state
-  let ignoreNextHistoryUpdate = false;
+  let unblockBlockerHistoryUpdate: (() => void) | undefined = undefined;
 
   // Initialize the router, all side effects should be kicked off from here.
@@ -1042,6 +1027,7 @@
         // Ignore this event if it was just us resetting the URL from a
         // blocked POP navigation
-        if (ignoreNextHistoryUpdate) {
-          ignoreNextHistoryUpdate = false;
+        if (unblockBlockerHistoryUpdate) {
+          unblockBlockerHistoryUpdate();
+          unblockBlockerHistoryUpdate = undefined;
           return;
         }
@@ -1065,5 +1051,7 @@
         if (blockerKey && delta != null) {
           // Restore the URL to match the current UI, but don't update router state
-          ignoreNextHistoryUpdate = true;
+          let nextHistoryUpdatePromise = new Promise<void>((resolve) => {
+            unblockBlockerHistoryUpdate = resolve;
+          });
           init.history.go(delta * -1);
 
@@ -1079,6 +1067,8 @@
                 location,
               });
-              // Re-do the same POP navigation we just blocked
-              init.history.go(delta);
+              // Re-do the same POP navigation we just blocked, after the url
+              // restoration is also complete.  See:
+              // https://github.com/remix-run/react-router/issues/11613
+              nextHistoryUpdatePromise.then(() => init.history.go(delta));
             },
             reset() {
@@ -1179,6 +1169,6 @@
       subscriber(state, {
         deletedFetchers: deletedFetchersKeys,
-        unstable_viewTransitionOpts: opts.viewTransitionOpts,
-        unstable_flushSync: opts.flushSync === true,
+        viewTransitionOpts: opts.viewTransitionOpts,
+        flushSync: opts.flushSync === true,
       })
     );
@@ -1403,5 +1393,5 @@
         : undefined;
 
-    let flushSync = (opts && opts.unstable_flushSync) === true;
+    let flushSync = (opts && opts.flushSync) === true;
 
     let blockerKey = shouldBlockNavigation({
@@ -1442,5 +1432,5 @@
       preventScrollReset,
       replace: opts && opts.replace,
-      enableViewTransition: opts && opts.unstable_viewTransition,
+      enableViewTransition: opts && opts.viewTransition,
       flushSync,
     });
@@ -1476,5 +1466,9 @@
       pendingAction || state.historyAction,
       state.navigation.location,
-      { overrideNavigation: state.navigation }
+      {
+        overrideNavigation: state.navigation,
+        // Proxy through any rending view transition
+        enableViewTransition: pendingViewTransitionEnabled === true,
+      }
     );
   }
@@ -1547,5 +1541,5 @@
     // mutation submission.
     //
-    // Ignore on initial page loads because since the initial load will always
+    // Ignore on initial page loads because since the initial hydration will always
     // be "same hash".  For example, on /page#hash and submit a <Form method="post">
     // which will default to a navigation to /page
@@ -1697,8 +1691,6 @@
         return { shortCircuited: true };
       } else if (discoverResult.type === "error") {
-        let { boundaryId, error } = handleDiscoverRouteError(
-          location.pathname,
-          discoverResult
-        );
+        let boundaryId = findNearestBoundary(discoverResult.partialMatches)
+          .route.id;
         return {
           matches: discoverResult.partialMatches,
@@ -1707,5 +1699,5 @@
             {
               type: ResultType.error,
-              error,
+              error: discoverResult.error,
             },
           ],
@@ -1746,9 +1738,11 @@
       let results = await callDataStrategy(
         "action",
+        state,
         request,
         [actionMatch],
-        matches
+        matches,
+        null
       );
-      result = results[0];
+      result = results[actionMatch.route.id];
 
       if (request.signal.aborted) {
@@ -1772,5 +1766,5 @@
         replace = location === state.location.pathname + state.location.search;
       }
-      await startRedirectNavigation(request, result, {
+      await startRedirectNavigation(request, result, true, {
         submission,
         replace,
@@ -1873,13 +1867,11 @@
         return { shortCircuited: true };
       } else if (discoverResult.type === "error") {
-        let { boundaryId, error } = handleDiscoverRouteError(
-          location.pathname,
-          discoverResult
-        );
+        let boundaryId = findNearestBoundary(discoverResult.partialMatches)
+          .route.id;
         return {
           matches: discoverResult.partialMatches,
           loaderData: {},
           errors: {
-            [boundaryId]: error,
+            [boundaryId]: discoverResult.error,
           },
         };
@@ -1969,7 +1961,5 @@
 
     revalidatingFetchers.forEach((rf) => {
-      if (fetchControllers.has(rf.key)) {
-        abortFetcher(rf.key);
-      }
+      abortFetcher(rf.key);
       if (rf.controller) {
         // Fetchers use an independent AbortController so that aborting a fetcher
@@ -1992,5 +1982,5 @@
     let { loaderResults, fetcherResults } =
       await callLoadersAndMaybeResolveData(
-        state.matches,
+        state,
         matches,
         matchesToLoad,
@@ -2012,18 +2002,23 @@
       );
     }
+
     revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
 
     // If any loaders returned a redirect Response, start a new REPLACE navigation
-    let redirect = findRedirect([...loaderResults, ...fetcherResults]);
+    let redirect = findRedirect(loaderResults);
     if (redirect) {
-      if (redirect.idx >= matchesToLoad.length) {
-        // If this redirect came from a fetcher make sure we mark it in
-        // fetchRedirectIds so it doesn't get revalidated on the next set of
-        // loader executions
-        let fetcherKey =
-          revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
-        fetchRedirectIds.add(fetcherKey);
-      }
-      await startRedirectNavigation(request, redirect.result, {
+      await startRedirectNavigation(request, redirect.result, true, {
+        replace,
+      });
+      return { shortCircuited: true };
+    }
+
+    redirect = findRedirect(fetcherResults);
+    if (redirect) {
+      // If this redirect came from a fetcher make sure we mark it in
+      // fetchRedirectIds so it doesn't get revalidated on the next set of
+      // loader executions
+      fetchRedirectIds.add(redirect.key);
+      await startRedirectNavigation(request, redirect.result, true, {
         replace,
       });
@@ -2035,5 +2030,4 @@
       state,
       matches,
-      matchesToLoad,
       loaderResults,
       pendingActionResult,
@@ -2055,11 +2049,7 @@
     });
 
-    // During partial hydration, preserve SSR errors for routes that don't re-run
+    // Preserve SSR errors during partial hydration
     if (future.v7_partialHydration && initialHydration && state.errors) {
-      Object.entries(state.errors)
-        .filter(([id]) => !matchesToLoad.some((m) => m.route.id === id))
-        .forEach(([routeId, error]) => {
-          errors = Object.assign(errors || {}, { [routeId]: error });
-        });
+      errors = { ...state.errors, ...errors };
     }
 
@@ -2125,6 +2115,7 @@
     }
 
-    if (fetchControllers.has(key)) abortFetcher(key);
-    let flushSync = (opts && opts.unstable_flushSync) === true;
+    abortFetcher(key);
+
+    let flushSync = (opts && opts.flushSync) === true;
 
     let routesToUse = inFlightDataRoutes || dataRoutes;
@@ -2170,5 +2161,5 @@
     let match = getTargetMatch(matches, path);
 
-    pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
+    let preventScrollReset = (opts && opts.preventScrollReset) === true;
 
     if (submission && isMutationMethod(submission.formMethod)) {
@@ -2181,4 +2172,5 @@
         fogOfWar.active,
         flushSync,
+        preventScrollReset,
         submission
       );
@@ -2197,4 +2189,5 @@
       fogOfWar.active,
       flushSync,
+      preventScrollReset,
       submission
     );
@@ -2211,4 +2204,5 @@
     isFogOfWar: boolean,
     flushSync: boolean,
+    preventScrollReset: boolean,
     submission: Submission
   ) {
@@ -2257,6 +2251,5 @@
         return;
       } else if (discoverResult.type === "error") {
-        let { error } = handleDiscoverRouteError(path, discoverResult);
-        setFetcherError(key, routeId, error, { flushSync });
+        setFetcherError(key, routeId, discoverResult.error, { flushSync });
         return;
       } else if (!discoverResult.matches) {
@@ -2284,9 +2277,11 @@
     let actionResults = await callDataStrategy(
       "action",
+      state,
       fetchRequest,
       [match],
-      requestMatches
+      requestMatches,
+      key
     );
-    let actionResult = actionResults[0];
+    let actionResult = actionResults[match.route.id];
 
     if (fetchRequest.signal.aborted) {
@@ -2321,6 +2316,7 @@
           fetchRedirectIds.add(key);
           updateFetcherState(key, getLoadingFetcher(submission));
-          return startRedirectNavigation(fetchRequest, actionResult, {
+          return startRedirectNavigation(fetchRequest, actionResult, false, {
             fetcherSubmission: submission,
+            preventScrollReset,
           });
         }
@@ -2392,7 +2388,5 @@
         );
         state.fetchers.set(staleKey, revalidatingFetcher);
-        if (fetchControllers.has(staleKey)) {
-          abortFetcher(staleKey);
-        }
+        abortFetcher(staleKey);
         if (rf.controller) {
           fetchControllers.set(staleKey, rf.controller);
@@ -2412,5 +2406,5 @@
     let { loaderResults, fetcherResults } =
       await callLoadersAndMaybeResolveData(
-        state.matches,
+        state,
         matches,
         matchesToLoad,
@@ -2432,15 +2426,26 @@
     revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
 
-    let redirect = findRedirect([...loaderResults, ...fetcherResults]);
+    let redirect = findRedirect(loaderResults);
     if (redirect) {
-      if (redirect.idx >= matchesToLoad.length) {
-        // If this redirect came from a fetcher make sure we mark it in
-        // fetchRedirectIds so it doesn't get revalidated on the next set of
-        // loader executions
-        let fetcherKey =
-          revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
-        fetchRedirectIds.add(fetcherKey);
-      }
-      return startRedirectNavigation(revalidationRequest, redirect.result);
+      return startRedirectNavigation(
+        revalidationRequest,
+        redirect.result,
+        false,
+        { preventScrollReset }
+      );
+    }
+
+    redirect = findRedirect(fetcherResults);
+    if (redirect) {
+      // If this redirect came from a fetcher make sure we mark it in
+      // fetchRedirectIds so it doesn't get revalidated on the next set of
+      // loader executions
+      fetchRedirectIds.add(redirect.key);
+      return startRedirectNavigation(
+        revalidationRequest,
+        redirect.result,
+        false,
+        { preventScrollReset }
+      );
     }
 
@@ -2448,6 +2453,5 @@
     let { loaderData, errors } = processLoaderData(
       state,
-      state.matches,
-      matchesToLoad,
+      matches,
       loaderResults,
       undefined,
@@ -2509,4 +2513,5 @@
     isFogOfWar: boolean,
     flushSync: boolean,
+    preventScrollReset: boolean,
     submission?: Submission
   ) {
@@ -2538,6 +2543,5 @@
         return;
       } else if (discoverResult.type === "error") {
-        let { error } = handleDiscoverRouteError(path, discoverResult);
-        setFetcherError(key, routeId, error, { flushSync });
+        setFetcherError(key, routeId, discoverResult.error, { flushSync });
         return;
       } else if (!discoverResult.matches) {
@@ -2561,9 +2565,11 @@
     let results = await callDataStrategy(
       "loader",
+      state,
       fetchRequest,
       [match],
-      matches
+      matches,
+      key
     );
-    let result = results[0];
+    let result = results[match.route.id];
 
     // Deferred isn't supported for fetcher loads, await everything and treat it
@@ -2603,5 +2609,7 @@
       } else {
         fetchRedirectIds.add(key);
-        await startRedirectNavigation(fetchRequest, result);
+        await startRedirectNavigation(fetchRequest, result, false, {
+          preventScrollReset,
+        });
         return;
       }
@@ -2642,11 +2650,14 @@
     request: Request,
     redirect: RedirectResult,
+    isNavigation: boolean,
     {
       submission,
       fetcherSubmission,
+      preventScrollReset,
       replace,
     }: {
       submission?: Submission;
       fetcherSubmission?: Submission;
+      preventScrollReset?: boolean;
       replace?: boolean;
     } = {}
@@ -2728,6 +2739,9 @@
           formAction: location,
         },
-        // Preserve this flag across redirects
-        preventScrollReset: pendingPreventScrollReset,
+        // Preserve these flags across redirects
+        preventScrollReset: preventScrollReset || pendingPreventScrollReset,
+        enableViewTransition: isNavigation
+          ? pendingViewTransitionEnabled
+          : undefined,
       });
     } else {
@@ -2742,6 +2756,9 @@
         // Send fetcher submissions through for shouldRevalidate
         fetcherSubmission,
-        // Preserve this flag across redirects
-        preventScrollReset: pendingPreventScrollReset,
+        // Preserve these flags across redirects
+        preventScrollReset: preventScrollReset || pendingPreventScrollReset,
+        enableViewTransition: isNavigation
+          ? pendingViewTransitionEnabled
+          : undefined,
       });
     }
@@ -2752,51 +2769,62 @@
   async function callDataStrategy(
     type: "loader" | "action",
+    state: RouterState,
     request: Request,
     matchesToLoad: AgnosticDataRouteMatch[],
-    matches: AgnosticDataRouteMatch[]
-  ): Promise<DataResult[]> {
+    matches: AgnosticDataRouteMatch[],
+    fetcherKey: string | null
+  ): Promise<Record<string, DataResult>> {
+    let results: Record<string, DataStrategyResult>;
+    let dataResults: Record<string, DataResult> = {};
     try {
-      let results = await callDataStrategyImpl(
+      results = await callDataStrategyImpl(
         dataStrategyImpl,
         type,
+        state,
         request,
         matchesToLoad,
         matches,
+        fetcherKey,
         manifest,
         mapRouteProperties
-      );
-
-      return await Promise.all(
-        results.map((result, i) => {
-          if (isRedirectHandlerResult(result)) {
-            let response = result.result as Response;
-            return {
-              type: ResultType.redirect,
-              response: normalizeRelativeRoutingRedirectResponse(
-                response,
-                request,
-                matchesToLoad[i].route.id,
-                matches,
-                basename,
-                future.v7_relativeSplatPath
-              ),
-            };
-          }
-
-          return convertHandlerResultToDataResult(result);
-        })
       );
     } catch (e) {
       // If the outer dataStrategy method throws, just return the error for all
       // matches - and it'll naturally bubble to the root
-      return matchesToLoad.map(() => ({
-        type: ResultType.error,
-        error: e,
-      }));
-    }
+      matchesToLoad.forEach((m) => {
+        dataResults[m.route.id] = {
+          type: ResultType.error,
+          error: e,
+        };
+      });
+      return dataResults;
+    }
+
+    for (let [routeId, result] of Object.entries(results)) {
+      if (isRedirectDataStrategyResultResult(result)) {
+        let response = result.result as Response;
+        dataResults[routeId] = {
+          type: ResultType.redirect,
+          response: normalizeRelativeRoutingRedirectResponse(
+            response,
+            request,
+            routeId,
+            matches,
+            basename,
+            future.v7_relativeSplatPath
+          ),
+        };
+      } else {
+        dataResults[routeId] = await convertDataStrategyResultToDataResult(
+          result
+        );
+      }
+    }
+
+    return dataResults;
   }
 
   async function callLoadersAndMaybeResolveData(
-    currentMatches: AgnosticDataRouteMatch[],
+    state: RouterState,
     matches: AgnosticDataRouteMatch[],
     matchesToLoad: AgnosticDataRouteMatch[],
@@ -2804,48 +2832,58 @@
     request: Request
   ) {
-    let [loaderResults, ...fetcherResults] = await Promise.all([
-      matchesToLoad.length
-        ? callDataStrategy("loader", request, matchesToLoad, matches)
-        : [],
-      ...fetchersToLoad.map((f) => {
+    let currentMatches = state.matches;
+
+    // Kick off loaders and fetchers in parallel
+    let loaderResultsPromise = callDataStrategy(
+      "loader",
+      state,
+      request,
+      matchesToLoad,
+      matches,
+      null
+    );
+
+    let fetcherResultsPromise = Promise.all(
+      fetchersToLoad.map(async (f) => {
         if (f.matches && f.match && f.controller) {
-          let fetcherRequest = createClientSideRequest(
-            init.history,
-            f.path,
-            f.controller.signal
+          let results = await callDataStrategy(
+            "loader",
+            state,
+            createClientSideRequest(init.history, f.path, f.controller.signal),
+            [f.match],
+            f.matches,
+            f.key
           );
-          return callDataStrategy(
-            "loader",
-            fetcherRequest,
-            [f.match],
-            f.matches
-          ).then((r) => r[0]);
+          let result = results[f.match.route.id];
+          // Fetcher results are keyed by fetcher key from here on out, not routeId
+          return { [f.key]: result };
         } else {
-          return Promise.resolve<DataResult>({
-            type: ResultType.error,
-            error: getInternalRouterError(404, {
-              pathname: f.path,
-            }),
+          return Promise.resolve({
+            [f.key]: {
+              type: ResultType.error,
+              error: getInternalRouterError(404, {
+                pathname: f.path,
+              }),
+            } as ErrorResult,
           });
         }
-      }),
-    ]);
+      })
+    );
+
+    let loaderResults = await loaderResultsPromise;
+    let fetcherResults = (await fetcherResultsPromise).reduce(
+      (acc, r) => Object.assign(acc, r),
+      {}
+    );
 
     await Promise.all([
-      resolveDeferredResults(
+      resolveNavigationDeferredResults(
+        matches,
+        loaderResults,
+        request.signal,
         currentMatches,
-        matchesToLoad,
-        loaderResults,
-        loaderResults.map(() => request.signal),
-        false,
         state.loaderData
       ),
-      resolveDeferredResults(
-        currentMatches,
-        fetchersToLoad.map((f) => f.match),
-        fetcherResults,
-        fetchersToLoad.map((f) => (f.controller ? f.controller.signal : null)),
-        true
-      ),
+      resolveFetcherDeferredResults(matches, fetcherResults, fetchersToLoad),
     ]);
 
@@ -2868,6 +2906,6 @@
       if (fetchControllers.has(key)) {
         cancelledFetcherLoads.add(key);
-        abortFetcher(key);
-      }
+      }
+      abortFetcher(key);
     });
   }
@@ -2952,7 +2990,8 @@
   function abortFetcher(key: string) {
     let controller = fetchControllers.get(key);
-    invariant(controller, `Expected fetch controller: ${key}`);
-    controller.abort();
-    fetchControllers.delete(key);
+    if (controller) {
+      controller.abort();
+      fetchControllers.delete(key);
+    }
   }
 
@@ -3078,21 +3117,4 @@
 
     return { notFoundMatches: matches, route, error };
-  }
-
-  function handleDiscoverRouteError(
-    pathname: string,
-    discoverResult: DiscoverRoutesErrorResult
-  ) {
-    return {
-      boundaryId: findNearestBoundary(discoverResult.partialMatches).route.id,
-      error: getInternalRouterError(400, {
-        type: "route-discovery",
-        pathname,
-        message:
-          discoverResult.error != null && "message" in discoverResult.error
-            ? discoverResult.error
-            : String(discoverResult.error),
-      }),
-    };
   }
 
@@ -3183,5 +3205,5 @@
     pathname: string
   ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {
-    if (patchRoutesOnMissImpl) {
+    if (patchRoutesOnNavigationImpl) {
       if (!matches) {
         let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>(
@@ -3194,12 +3216,8 @@
         return { active: true, matches: fogMatches || [] };
       } else {
-        let leafRoute = matches[matches.length - 1].route;
-        if (
-          leafRoute.path &&
-          (leafRoute.path === "*" || leafRoute.path.endsWith("/*"))
-        ) {
-          // If we matched a splat, it might only be because we haven't yet fetched
-          // the children that would match with a higher score, so let's fetch
-          // around and find out
+        if (Object.keys(matches[0].params).length > 0) {
+          // If we matched a dynamic param or a splat, it might only be because
+          // we haven't yet discovered other routes that would match with a
+          // higher score.  Call patchRoutesOnNavigation just to be sure
           let partialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
             routesToUse,
@@ -3236,23 +3254,28 @@
     signal: AbortSignal
   ): Promise<DiscoverRoutesResult> {
+    if (!patchRoutesOnNavigationImpl) {
+      return { type: "success", matches };
+    }
+
     let partialMatches: AgnosticDataRouteMatch[] | null = matches;
-    let route =
-      partialMatches.length > 0
-        ? partialMatches[partialMatches.length - 1].route
-        : null;
     while (true) {
       let isNonHMR = inFlightDataRoutes == null;
       let routesToUse = inFlightDataRoutes || dataRoutes;
+      let localManifest = manifest;
       try {
-        await loadLazyRouteChildren(
-          patchRoutesOnMissImpl!,
-          pathname,
-          partialMatches,
-          routesToUse,
-          manifest,
-          mapRouteProperties,
-          pendingPatchRoutes,
-          signal
-        );
+        await patchRoutesOnNavigationImpl({
+          path: pathname,
+          matches: partialMatches,
+          patch: (routeId, children) => {
+            if (signal.aborted) return;
+            patchRoutesImpl(
+              routeId,
+              children,
+              routesToUse,
+              localManifest,
+              mapRouteProperties
+            );
+          },
+        });
       } catch (e) {
         return { type: "error", error: e, partialMatches };
@@ -3264,5 +3287,5 @@
         // HMR will already update the identity and reflow when it lands
         // `inFlightDataRoutes` in `completeNavigation`
-        if (isNonHMR) {
+        if (isNonHMR && !signal.aborted) {
           dataRoutes = [...dataRoutes];
         }
@@ -3274,24 +3297,6 @@
 
       let newMatches = matchRoutes(routesToUse, pathname, basename);
-      let matchedSplat = false;
       if (newMatches) {
-        let leafRoute = newMatches[newMatches.length - 1].route;
-
-        if (leafRoute.index) {
-          // If we found an index route, we can stop
-          return { type: "success", matches: newMatches };
-        }
-
-        if (leafRoute.path && leafRoute.path.length > 0) {
-          if (leafRoute.path === "*") {
-            // If we found a splat route, we can't be sure there's not a
-            // higher-scoring route down some partial matches trail so we need
-            // to check that out
-            matchedSplat = true;
-          } else {
-            // If we found a non-splat route, we can stop
-            return { type: "success", matches: newMatches };
-          }
-        }
+        return { type: "success", matches: newMatches };
       }
 
@@ -3303,21 +3308,16 @@
       );
 
-      // If we are no longer partially matching anything, this was either a
-      // legit splat match above, or it's a 404.  Also avoid loops if the
-      // second pass results in the same partial matches
+      // Avoid loops if the second pass results in the same partial matches
       if (
         !newPartialMatches ||
-        partialMatches.map((m) => m.route.id).join("-") ===
-          newPartialMatches.map((m) => m.route.id).join("-")
+        (partialMatches.length === newPartialMatches.length &&
+          partialMatches.every(
+            (m, i) => m.route.id === newPartialMatches![i].route.id
+          ))
       ) {
-        return { type: "success", matches: matchedSplat ? newMatches : null };
+        return { type: "success", matches: null };
       }
 
       partialMatches = newPartialMatches;
-      route = partialMatches[partialMatches.length - 1].route;
-      if (route.path === "*") {
-        // The splat is still our most accurate partial, so run with it
-        return { type: "success", matches: partialMatches };
-      }
     }
   }
@@ -3493,9 +3493,9 @@
       requestContext,
       skipLoaderErrorBubbling,
-      unstable_dataStrategy,
+      dataStrategy,
     }: {
       requestContext?: unknown;
       skipLoaderErrorBubbling?: boolean;
-      unstable_dataStrategy?: DataStrategyFunction;
+      dataStrategy?: DataStrategyFunction;
     } = {}
   ): Promise<StaticHandlerContext | Response> {
@@ -3549,5 +3549,5 @@
       matches,
       requestContext,
-      unstable_dataStrategy || null,
+      dataStrategy || null,
       skipLoaderErrorBubbling === true,
       null
@@ -3594,9 +3594,9 @@
       routeId,
       requestContext,
-      unstable_dataStrategy,
+      dataStrategy,
     }: {
       requestContext?: unknown;
       routeId?: string;
-      unstable_dataStrategy?: DataStrategyFunction;
+      dataStrategy?: DataStrategyFunction;
     } = {}
   ): Promise<any> {
@@ -3632,5 +3632,5 @@
       matches,
       requestContext,
-      unstable_dataStrategy || null,
+      dataStrategy || null,
       false,
       match
@@ -3671,5 +3671,5 @@
     matches: AgnosticDataRouteMatch[],
     requestContext: unknown,
-    unstable_dataStrategy: DataStrategyFunction | null,
+    dataStrategy: DataStrategyFunction | null,
     skipLoaderErrorBubbling: boolean,
     routeMatch: AgnosticDataRouteMatch | null
@@ -3687,5 +3687,5 @@
           routeMatch || getTargetMatch(matches, location),
           requestContext,
-          unstable_dataStrategy,
+          dataStrategy,
           skipLoaderErrorBubbling,
           routeMatch != null
@@ -3698,5 +3698,5 @@
         matches,
         requestContext,
-        unstable_dataStrategy,
+        dataStrategy,
         skipLoaderErrorBubbling,
         routeMatch
@@ -3711,7 +3711,7 @@
     } catch (e) {
       // If the user threw/returned a Response in callLoaderOrAction for a
-      // `queryRoute` call, we throw the `HandlerResult` to bail out early
+      // `queryRoute` call, we throw the `DataStrategyResult` to bail out early
       // and then return or throw the raw Response here accordingly
-      if (isHandlerResult(e) && isResponse(e.result)) {
+      if (isDataStrategyResult(e) && isResponse(e.result)) {
         if (e.type === ResultType.error) {
           throw e.result;
@@ -3733,5 +3733,5 @@
     actionMatch: AgnosticDataRouteMatch,
     requestContext: unknown,
-    unstable_dataStrategy: DataStrategyFunction | null,
+    dataStrategy: DataStrategyFunction | null,
     skipLoaderErrorBubbling: boolean,
     isRouteRequest: boolean
@@ -3760,7 +3760,7 @@
         isRouteRequest,
         requestContext,
-        unstable_dataStrategy
+        dataStrategy
       );
-      result = results[0];
+      result = results[actionMatch.route.id];
 
       if (request.signal.aborted) {
@@ -3832,5 +3832,5 @@
         matches,
         requestContext,
-        unstable_dataStrategy,
+        dataStrategy,
         skipLoaderErrorBubbling,
         null,
@@ -3857,5 +3857,5 @@
       matches,
       requestContext,
-      unstable_dataStrategy,
+      dataStrategy,
       skipLoaderErrorBubbling,
       null
@@ -3879,5 +3879,5 @@
     matches: AgnosticDataRouteMatch[],
     requestContext: unknown,
-    unstable_dataStrategy: DataStrategyFunction | null,
+    dataStrategy: DataStrategyFunction | null,
     skipLoaderErrorBubbling: boolean,
     routeMatch: AgnosticDataRouteMatch | null,
@@ -3942,5 +3942,5 @@
       isRouteRequest,
       requestContext,
-      unstable_dataStrategy
+      dataStrategy
     );
 
@@ -3953,5 +3953,4 @@
     let context = processRouteLoaderData(
       matches,
-      matchesToLoad,
       results,
       pendingActionResult,
@@ -3989,12 +3988,14 @@
     isRouteRequest: boolean,
     requestContext: unknown,
-    unstable_dataStrategy: DataStrategyFunction | null
-  ): Promise<DataResult[]> {
+    dataStrategy: DataStrategyFunction | null
+  ): Promise<Record<string, DataResult>> {
     let results = await callDataStrategyImpl(
-      unstable_dataStrategy || defaultDataStrategy,
+      dataStrategy || defaultDataStrategy,
       type,
+      null,
       request,
       matchesToLoad,
       matches,
+      null,
       manifest,
       mapRouteProperties,
@@ -4002,7 +4003,12 @@
     );
 
-    return await Promise.all(
-      results.map((result, i) => {
-        if (isRedirectHandlerResult(result)) {
+    let dataResults: Record<string, DataResult> = {};
+    await Promise.all(
+      matches.map(async (match) => {
+        if (!(match.route.id in results)) {
+          return;
+        }
+        let result = results[match.route.id];
+        if (isRedirectDataStrategyResultResult(result)) {
           let response = result.result as Response;
           // Throw redirects and let the server handle them with an HTTP redirect
@@ -4010,5 +4016,5 @@
             response,
             request,
-            matchesToLoad[i].route.id,
+            match.route.id,
             matches,
             basename,
@@ -4022,7 +4028,9 @@
         }
 
-        return convertHandlerResultToDataResult(result);
+        dataResults[match.route.id] =
+          await convertDataStrategyResultToDataResult(result);
       })
     );
+    return dataResults;
   }
 
@@ -4126,14 +4134,21 @@
   }
 
-  // Add an ?index param for matched index routes if we don't already have one
-  if (
-    (to == null || to === "" || to === ".") &&
-    activeRouteMatch &&
-    activeRouteMatch.route.index &&
-    !hasNakedIndexQuery(path.search)
-  ) {
-    path.search = path.search
-      ? path.search.replace(/^\?/, "?index&")
-      : "?index";
+  // Account for `?index` params when routing to the current location
+  if ((to == null || to === "" || to === ".") && activeRouteMatch) {
+    let nakedIndex = hasNakedIndexQuery(path.search);
+    if (activeRouteMatch.route.index && !nakedIndex) {
+      // Add one when we're targeting an index route
+      path.search = path.search
+        ? path.search.replace(/^\?/, "?index&")
+        : "?index";
+    } else if (!activeRouteMatch.route.index && nakedIndex) {
+      // Remove existing ones when we're not
+      let params = new URLSearchParams(path.search);
+      let indexValues = params.getAll("index");
+      params.delete("index");
+      indexValues.filter((v) => v).forEach((v) => params.append("index", v));
+      let qs = params.toString();
+      path.search = qs ? `?${qs}` : "";
+    }
   }
 
@@ -4299,18 +4314,16 @@
 }
 
-// Filter out all routes below any caught error as they aren't going to
+// Filter out all routes at/below any caught error as they aren't going to
 // render so we don't need to load them
 function getLoaderMatchesUntilBoundary(
   matches: AgnosticDataRouteMatch[],
-  boundaryId: string
+  boundaryId: string,
+  includeBoundary = false
 ) {
-  let boundaryMatches = matches;
-  if (boundaryId) {
-    let index = matches.findIndex((m) => m.route.id === boundaryId);
-    if (index >= 0) {
-      boundaryMatches = matches.slice(0, index);
-    }
-  }
-  return boundaryMatches;
+  let index = matches.findIndex((m) => m.route.id === boundaryId);
+  if (index >= 0) {
+    return matches.slice(0, includeBoundary ? index + 1 : index);
+  }
+  return matches;
 }
 
@@ -4321,5 +4334,5 @@
   submission: Submission | undefined,
   location: Location,
-  isInitialLoad: boolean,
+  initialHydration: boolean,
   skipActionErrorRevalidation: boolean,
   isRevalidationRequired: boolean,
@@ -4342,11 +4355,24 @@
 
   // Pick navigation matches that are net-new or qualify for revalidation
-  let boundaryId =
-    pendingActionResult && isErrorResult(pendingActionResult[1])
-      ? pendingActionResult[0]
-      : undefined;
-  let boundaryMatches = boundaryId
-    ? getLoaderMatchesUntilBoundary(matches, boundaryId)
-    : matches;
+  let boundaryMatches = matches;
+  if (initialHydration && state.errors) {
+    // On initial hydration, only consider matches up to _and including_ the boundary.
+    // This is inclusive to handle cases where a server loader ran successfully,
+    // a child server loader bubbled up to this route, but this route has
+    // `clientLoader.hydrate` so we want to still run the `clientLoader` so that
+    // we have a complete version of `loaderData`
+    boundaryMatches = getLoaderMatchesUntilBoundary(
+      matches,
+      Object.keys(state.errors)[0],
+      true
+    );
+  } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) {
+    // If an action threw an error, we call loaders up to, but not including the
+    // boundary
+    boundaryMatches = getLoaderMatchesUntilBoundary(
+      matches,
+      pendingActionResult[0]
+    );
+  }
 
   // Don't revalidate loaders by default after action 4xx/5xx responses
@@ -4370,13 +4396,6 @@
     }
 
-    if (isInitialLoad) {
-      if (typeof route.loader !== "function" || route.loader.hydrate) {
-        return true;
-      }
-      return (
-        state.loaderData[route.id] === undefined &&
-        // Don't re-run if the loader ran and threw an error
-        (!state.errors || state.errors[route.id] === undefined)
-      );
+    if (initialHydration) {
+      return shouldLoadRouteOnHydration(route, state.loaderData, state.errors);
     }
 
@@ -4420,10 +4439,10 @@
   fetchLoadMatches.forEach((f, key) => {
     // Don't revalidate:
-    //  - on initial load (shouldn't be any fetchers then anyway)
+    //  - on initial hydration (shouldn't be any fetchers then anyway)
     //  - if fetcher won't be present in the subsequent render
     //    - no longer matches the URL (v7_fetcherPersist=false)
     //    - was unmounted but persisted due to v7_fetcherPersist=true
     if (
-      isInitialLoad ||
+      initialHydration ||
       !matches.some((m) => m.route.id === f.routeId) ||
       deletedFetchers.has(key)
@@ -4505,4 +4524,36 @@
 }
 
+function shouldLoadRouteOnHydration(
+  route: AgnosticDataRouteObject,
+  loaderData: RouteData | null | undefined,
+  errors: RouteData | null | undefined
+) {
+  // We dunno if we have a loader - gotta find out!
+  if (route.lazy) {
+    return true;
+  }
+
+  // No loader, nothing to initialize
+  if (!route.loader) {
+    return false;
+  }
+
+  let hasData = loaderData != null && loaderData[route.id] !== undefined;
+  let hasError = errors != null && errors[route.id] !== undefined;
+
+  // Don't run if we error'd during SSR
+  if (!hasData && hasError) {
+    return false;
+  }
+
+  // Explicitly opting-in to running on hydration
+  if (typeof route.loader === "function" && route.loader.hydrate === true) {
+    return true;
+  }
+
+  // Otherwise, run if we're not yet initialized with anything
+  return !hasData && !hasError;
+}
+
 function isNewLoader(
   currentLoaderData: RouteData,
@@ -4554,48 +4605,4 @@
 }
 
-/**
- * Idempotent utility to execute patchRoutesOnMiss() to lazily load route
- * definitions and update the routes/routeManifest
- */
-async function loadLazyRouteChildren(
-  patchRoutesOnMissImpl: AgnosticPatchRoutesOnMissFunction,
-  path: string,
-  matches: AgnosticDataRouteMatch[],
-  routes: AgnosticDataRouteObject[],
-  manifest: RouteManifest,
-  mapRouteProperties: MapRoutePropertiesFunction,
-  pendingRouteChildren: Map<string, ReturnType<typeof patchRoutesOnMissImpl>>,
-  signal: AbortSignal
-) {
-  let key = [path, ...matches.map((m) => m.route.id)].join("-");
-  try {
-    let pending = pendingRouteChildren.get(key);
-    if (!pending) {
-      pending = patchRoutesOnMissImpl({
-        path,
-        matches,
-        patch: (routeId, children) => {
-          if (!signal.aborted) {
-            patchRoutesImpl(
-              routeId,
-              children,
-              routes,
-              manifest,
-              mapRouteProperties
-            );
-          }
-        },
-      });
-      pendingRouteChildren.set(key, pending);
-    }
-
-    if (pending && isPromise<AgnosticRouteObject[]>(pending)) {
-      await pending;
-    }
-  } finally {
-    pendingRouteChildren.delete(key);
-  }
-}
-
 function patchRoutesImpl(
   routeId: string | null,
@@ -4605,4 +4612,5 @@
   mapRouteProperties: MapRoutePropertiesFunction
 ) {
+  let childrenToPatch: AgnosticDataRouteObject[];
   if (routeId) {
     let route = manifest[routeId];
@@ -4611,24 +4619,70 @@
       `No route found to patch children into: routeId = ${routeId}`
     );
-    let dataChildren = convertRoutesToDataRoutes(
-      children,
-      mapRouteProperties,
-      [routeId, "patch", String(route.children?.length || "0")],
-      manifest
-    );
-    if (route.children) {
-      route.children.push(...dataChildren);
-    } else {
-      route.children = dataChildren;
-    }
+    if (!route.children) {
+      route.children = [];
+    }
+    childrenToPatch = route.children;
   } else {
-    let dataChildren = convertRoutesToDataRoutes(
-      children,
-      mapRouteProperties,
-      ["patch", String(routesToUse.length || "0")],
-      manifest
-    );
-    routesToUse.push(...dataChildren);
-  }
+    childrenToPatch = routesToUse;
+  }
+
+  // Don't patch in routes we already know about so that `patch` is idempotent
+  // to simplify user-land code. This is useful because we re-call the
+  // `patchRoutesOnNavigation` function for matched routes with params.
+  let uniqueChildren = children.filter(
+    (newRoute) =>
+      !childrenToPatch.some((existingRoute) =>
+        isSameRoute(newRoute, existingRoute)
+      )
+  );
+
+  let newRoutes = convertRoutesToDataRoutes(
+    uniqueChildren,
+    mapRouteProperties,
+    [routeId || "_", "patch", String(childrenToPatch?.length || "0")],
+    manifest
+  );
+
+  childrenToPatch.push(...newRoutes);
+}
+
+function isSameRoute(
+  newRoute: AgnosticRouteObject,
+  existingRoute: AgnosticRouteObject
+): boolean {
+  // Most optimal check is by id
+  if (
+    "id" in newRoute &&
+    "id" in existingRoute &&
+    newRoute.id === existingRoute.id
+  ) {
+    return true;
+  }
+
+  // Second is by pathing differences
+  if (
+    !(
+      newRoute.index === existingRoute.index &&
+      newRoute.path === existingRoute.path &&
+      newRoute.caseSensitive === existingRoute.caseSensitive
+    )
+  ) {
+    return false;
+  }
+
+  // Pathless layout routes are trickier since we need to check children.
+  // If they have no children then they're the same as far as we can tell
+  if (
+    (!newRoute.children || newRoute.children.length === 0) &&
+    (!existingRoute.children || existingRoute.children.length === 0)
+  ) {
+    return true;
+  }
+
+  // Otherwise, we look to see if every child in the new route is already
+  // represented in the existing route's children
+  return newRoute.children!.every((aChild, i) =>
+    existingRoute.children?.some((bChild) => isSameRoute(aChild, bChild))
+  );
 }
 
@@ -4712,8 +4766,14 @@
 
 // Default implementation of `dataStrategy` which fetches all loaders in parallel
-function defaultDataStrategy(
-  opts: DataStrategyFunctionArgs
-): ReturnType<DataStrategyFunction> {
-  return Promise.all(opts.matches.map((m) => m.resolve()));
+async function defaultDataStrategy({
+  matches,
+}: DataStrategyFunctionArgs): ReturnType<DataStrategyFunction> {
+  let matchesToLoad = matches.filter((m) => m.shouldLoad);
+  let results = await Promise.all(matchesToLoad.map((m) => m.resolve()));
+  return results.reduce(
+    (acc, result, i) =>
+      Object.assign(acc, { [matchesToLoad[i].route.id]: result }),
+    {}
+  );
 }
 
@@ -4721,16 +4781,52 @@
   dataStrategyImpl: DataStrategyFunction,
   type: "loader" | "action",
+  state: RouterState | null,
   request: Request,
   matchesToLoad: AgnosticDataRouteMatch[],
   matches: AgnosticDataRouteMatch[],
+  fetcherKey: string | null,
   manifest: RouteManifest,
   mapRouteProperties: MapRoutePropertiesFunction,
   requestContext?: unknown
-): Promise<HandlerResult[]> {
-  let routeIdsToLoad = matchesToLoad.reduce(
-    (acc, m) => acc.add(m.route.id),
-    new Set<string>()
+): Promise<Record<string, DataStrategyResult>> {
+  let loadRouteDefinitionsPromises = matches.map((m) =>
+    m.route.lazy
+      ? loadLazyRouteModule(m.route, mapRouteProperties, manifest)
+      : undefined
   );
-  let loadedMatches = new Set<string>();
+
+  let dsMatches = matches.map((match, i) => {
+    let loadRoutePromise = loadRouteDefinitionsPromises[i];
+    let shouldLoad = matchesToLoad.some((m) => m.route.id === match.route.id);
+    // `resolve` encapsulates route.lazy(), executing the loader/action,
+    // and mapping return values/thrown errors to a `DataStrategyResult`.  Users
+    // can pass a callback to take fine-grained control over the execution
+    // of the loader/action
+    let resolve: DataStrategyMatch["resolve"] = async (handlerOverride) => {
+      if (
+        handlerOverride &&
+        request.method === "GET" &&
+        (match.route.lazy || match.route.loader)
+      ) {
+        shouldLoad = true;
+      }
+      return shouldLoad
+        ? callLoaderOrAction(
+            type,
+            request,
+            match,
+            loadRoutePromise,
+            handlerOverride,
+            requestContext
+          )
+        : Promise.resolve({ type: ResultType.data, result: undefined });
+    };
+
+    return {
+      ...match,
+      shouldLoad,
+      resolve,
+    };
+  });
 
   // Send all matches here to allow for a middleware-type implementation.
@@ -4738,49 +4834,21 @@
   // back out below.
   let results = await dataStrategyImpl({
-    matches: matches.map((match) => {
-      let shouldLoad = routeIdsToLoad.has(match.route.id);
-      // `resolve` encapsulates the route.lazy, executing the
-      // loader/action, and mapping return values/thrown errors to a
-      // HandlerResult.  Users can pass a callback to take fine-grained control
-      // over the execution of the loader/action
-      let resolve: DataStrategyMatch["resolve"] = (handlerOverride) => {
-        loadedMatches.add(match.route.id);
-        return shouldLoad
-          ? callLoaderOrAction(
-              type,
-              request,
-              match,
-              manifest,
-              mapRouteProperties,
-              handlerOverride,
-              requestContext
-            )
-          : Promise.resolve({ type: ResultType.data, result: undefined });
-      };
-
-      return {
-        ...match,
-        shouldLoad,
-        resolve,
-      };
-    }),
+    matches: dsMatches,
     request,
     params: matches[0].params,
+    fetcherKey,
     context: requestContext,
   });
 
-  // Throw if any loadRoute implementations not called since they are what
-  // ensures a route is fully loaded
-  matches.forEach((m) =>
-    invariant(
-      loadedMatches.has(m.route.id),
-      `\`match.resolve()\` was not called for route id "${m.route.id}". ` +
-        "You must call `match.resolve()` on every match passed to " +
-        "`dataStrategy` to ensure all routes are properly loaded."
-    )
-  );
-
-  // Filter out any middleware-only matches for which we didn't need to run handlers
-  return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
+  // Wait for all routes to load here but 'swallow the error since we want
+  // it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` -
+  // called from `match.resolve()`
+  try {
+    await Promise.all(loadRouteDefinitionsPromises);
+  } catch (e) {
+    // No-op
+  }
+
+  return results;
 }
 
@@ -4790,20 +4858,19 @@
   request: Request,
   match: AgnosticDataRouteMatch,
-  manifest: RouteManifest,
-  mapRouteProperties: MapRoutePropertiesFunction,
+  loadRoutePromise: Promise<void> | undefined,
   handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
   staticContext?: unknown
-): Promise<HandlerResult> {
-  let result: HandlerResult;
+): Promise<DataStrategyResult> {
+  let result: DataStrategyResult;
   let onReject: (() => void) | undefined;
 
   let runHandler = (
     handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
-  ): Promise<HandlerResult> => {
+  ): Promise<DataStrategyResult> => {
     // Setup a promise we can race against so that abort signals short circuit
     let reject: () => void;
-    // This will never resolve so safe to type it as Promise<HandlerResult> to
+    // This will never resolve so safe to type it as Promise<DataStrategyResult> to
     // satisfy the function return value
-    let abortPromise = new Promise<HandlerResult>((_, r) => (reject = r));
+    let abortPromise = new Promise<DataStrategyResult>((_, r) => (reject = r));
     onReject = () => reject();
     request.signal.addEventListener("abort", onReject);
@@ -4828,17 +4895,14 @@
     };
 
-    let handlerPromise: Promise<HandlerResult>;
-    if (handlerOverride) {
-      handlerPromise = handlerOverride((ctx: unknown) => actualHandler(ctx));
-    } else {
-      handlerPromise = (async () => {
-        try {
-          let val = await actualHandler();
-          return { type: "data", result: val };
-        } catch (e) {
-          return { type: "error", result: e };
-        }
-      })();
-    }
+    let handlerPromise: Promise<DataStrategyResult> = (async () => {
+      try {
+        let val = await (handlerOverride
+          ? handlerOverride((ctx: unknown) => actualHandler(ctx))
+          : actualHandler());
+        return { type: "data", result: val };
+      } catch (e) {
+        return { type: "error", result: e };
+      }
+    })();
 
     return Promise.race([handlerPromise, abortPromise]);
@@ -4848,5 +4912,6 @@
     let handler = match.route[type];
 
-    if (match.route.lazy) {
+    // If we have a route.lazy promise, await that first
+    if (loadRoutePromise) {
       if (handler) {
         // Run statically defined handler in parallel with lazy()
@@ -4859,5 +4924,5 @@
             handlerError = e;
           }),
-          loadLazyRouteModule(match.route, mapRouteProperties, manifest),
+          loadRoutePromise,
         ]);
         if (handlerError !== undefined) {
@@ -4867,5 +4932,5 @@
       } else {
         // Load lazy route module, then run any returned handler
-        await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
+        await loadRoutePromise;
 
         handler = match.route[type];
@@ -4907,5 +4972,5 @@
   } catch (e) {
     // We should already be catching and converting normal handler executions to
-    // HandlerResults and returning them, so anything that throws here is an
+    // DataStrategyResults and returning them, so anything that throws here is an
     // unexpected error we still need to wrap
     return { type: ResultType.error, result: e };
@@ -4919,8 +4984,8 @@
 }
 
-async function convertHandlerResultToDataResult(
-  handlerResult: HandlerResult
+async function convertDataStrategyResultToDataResult(
+  dataStrategyResult: DataStrategyResult
 ): Promise<DataResult> {
-  let { result, type } = handlerResult;
+  let { result, type } = dataStrategyResult;
 
   if (isResponse(result)) {
@@ -4971,5 +5036,5 @@
       }
 
-      // Convert thrown unstable_data() to ErrorResponse instances
+      // Convert thrown data() to ErrorResponse instances
       result = new ErrorResponseImpl(
         result.init?.status || 500,
@@ -5124,6 +5189,5 @@
 function processRouteLoaderData(
   matches: AgnosticDataRouteMatch[],
-  matchesToLoad: AgnosticDataRouteMatch[],
-  results: DataResult[],
+  results: Record<string, DataResult>,
   pendingActionResult: PendingActionResult | undefined,
   activeDeferreds: Map<string, DeferredData>,
@@ -5147,6 +5211,10 @@
 
   // Process loader results into state.loaderData/state.errors
-  results.forEach((result, index) => {
-    let id = matchesToLoad[index].route.id;
+  matches.forEach((match) => {
+    if (!(match.route.id in results)) {
+      return;
+    }
+    let id = match.route.id;
+    let result = results[id];
     invariant(
       !isRedirectResult(result),
@@ -5240,9 +5308,8 @@
   state: RouterState,
   matches: AgnosticDataRouteMatch[],
-  matchesToLoad: AgnosticDataRouteMatch[],
-  results: DataResult[],
+  results: Record<string, DataResult>,
   pendingActionResult: PendingActionResult | undefined,
   revalidatingFetchers: RevalidatingFetcher[],
-  fetcherResults: DataResult[],
+  fetcherResults: Record<string, DataResult>,
   activeDeferreds: Map<string, DeferredData>
 ): {
@@ -5252,5 +5319,4 @@
   let { loaderData, errors } = processRouteLoaderData(
     matches,
-    matchesToLoad,
     results,
     pendingActionResult,
@@ -5260,16 +5326,13 @@
 
   // Process results from our revalidating fetchers
-  for (let index = 0; index < revalidatingFetchers.length; index++) {
-    let { key, match, controller } = revalidatingFetchers[index];
-    invariant(
-      fetcherResults !== undefined && fetcherResults[index] !== undefined,
-      "Did not find corresponding fetcher result"
-    );
-    let result = fetcherResults[index];
+  revalidatingFetchers.forEach((rf) => {
+    let { key, match, controller } = rf;
+    let result = fetcherResults[key];
+    invariant(result, "Did not find corresponding fetcher result");
 
     // Process fetcher non-redirect errors
     if (controller && controller.signal.aborted) {
       // Nothing to do for aborted fetchers
-      continue;
+      return;
     } else if (isErrorResult(result)) {
       let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
@@ -5293,5 +5356,5 @@
       state.fetchers.set(key, doneFetcher);
     }
-  }
+  });
 
   return { loaderData, errors };
@@ -5400,5 +5463,5 @@
     routeId?: string;
     method?: string;
-    type?: "defer-action" | "invalid-body" | "route-discovery";
+    type?: "defer-action" | "invalid-body";
     message?: string;
   } = {}
@@ -5409,9 +5472,5 @@
   if (status === 400) {
     statusText = "Bad Request";
-    if (type === "route-discovery") {
-      errorMessage =
-        `Unable to match URL "${pathname}" - the \`unstable_patchRoutesOnMiss()\` ` +
-        `function threw the following error:\n${message}`;
-    } else if (method && pathname && routeId) {
+    if (method && pathname && routeId) {
       errorMessage =
         `You made a ${method} request to "${pathname}" but ` +
@@ -5451,10 +5510,11 @@
 // Find any returned redirect errors, starting from the lowest match
 function findRedirect(
-  results: DataResult[]
-): { result: RedirectResult; idx: number } | undefined {
-  for (let i = results.length - 1; i >= 0; i--) {
-    let result = results[i];
+  results: Record<string, DataResult>
+): { key: string; result: RedirectResult } | undefined {
+  let entries = Object.entries(results);
+  for (let i = entries.length - 1; i >= 0; i--) {
+    let [key, result] = entries[i];
     if (isRedirectResult(result)) {
-      return { result, idx: i };
+      return { key, result };
     }
   }
@@ -5491,5 +5551,5 @@
 }
 
-function isHandlerResult(result: unknown): result is HandlerResult {
+function isDataStrategyResult(result: unknown): result is DataStrategyResult {
   return (
     result != null &&
@@ -5501,5 +5561,5 @@
 }
 
-function isRedirectHandlerResult(result: HandlerResult) {
+function isRedirectDataStrategyResultResult(result: DataStrategyResult) {
   return (
     isResponse(result.result) && redirectStatusCodes.has(result.result.status)
@@ -5574,15 +5634,15 @@
 }
 
-async function resolveDeferredResults(
+async function resolveNavigationDeferredResults(
+  matches: (AgnosticDataRouteMatch | null)[],
+  results: Record<string, DataResult>,
+  signal: AbortSignal,
   currentMatches: AgnosticDataRouteMatch[],
-  matchesToLoad: (AgnosticDataRouteMatch | null)[],
-  results: DataResult[],
-  signals: (AbortSignal | null)[],
-  isFetcher: boolean,
-  currentLoaderData?: RouteData
+  currentLoaderData: RouteData
 ) {
-  for (let index = 0; index < results.length; index++) {
-    let result = results[index];
-    let match = matchesToLoad[index];
+  let entries = Object.entries(results);
+  for (let index = 0; index < entries.length; index++) {
+    let [routeId, result] = entries[index];
+    let match = matches.find((m) => m?.route.id === routeId);
     // If we don't have a match, then we can have a deferred result to do
     // anything with.  This is for revalidating fetchers where the route was
@@ -5600,18 +5660,48 @@
       (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
 
-    if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) {
+    if (isDeferredResult(result) && isRevalidatingLoader) {
       // Note: we do not have to touch activeDeferreds here since we race them
       // against the signal in resolveDeferredData and they'll get aborted
       // there if needed
-      let signal = signals[index];
-      invariant(
-        signal,
-        "Expected an AbortSignal for revalidating fetcher deferred result"
-      );
-      await resolveDeferredData(result, signal, isFetcher).then((result) => {
+      await resolveDeferredData(result, signal, false).then((result) => {
         if (result) {
-          results[index] = result || results[index];
+          results[routeId] = result;
         }
       });
+    }
+  }
+}
+
+async function resolveFetcherDeferredResults(
+  matches: (AgnosticDataRouteMatch | null)[],
+  results: Record<string, DataResult>,
+  revalidatingFetchers: RevalidatingFetcher[]
+) {
+  for (let index = 0; index < revalidatingFetchers.length; index++) {
+    let { key, routeId, controller } = revalidatingFetchers[index];
+    let result = results[key];
+    let match = matches.find((m) => m?.route.id === routeId);
+    // If we don't have a match, then we can have a deferred result to do
+    // anything with.  This is for revalidating fetchers where the route was
+    // removed during HMR
+    if (!match) {
+      continue;
+    }
+
+    if (isDeferredResult(result)) {
+      // Note: we do not have to touch activeDeferreds here since we race them
+      // against the signal in resolveDeferredData and they'll get aborted
+      // there if needed
+      invariant(
+        controller,
+        "Expected an AbortController for revalidating fetcher deferred result"
+      );
+      await resolveDeferredData(result, controller.signal, true).then(
+        (result) => {
+          if (result) {
+            results[key] = result;
+          }
+        }
+      );
     }
   }
