@remix-run/router 1.15.3 → 1.16.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/router.ts CHANGED
@@ -8,11 +8,13 @@ import {
8
8
  warning,
9
9
  } from "./history";
10
10
  import type {
11
- ActionFunction,
12
11
  AgnosticDataRouteMatch,
13
12
  AgnosticDataRouteObject,
13
+ DataStrategyMatch,
14
14
  AgnosticRouteObject,
15
15
  DataResult,
16
+ DataStrategyFunction,
17
+ DataStrategyFunctionArgs,
16
18
  DeferredData,
17
19
  DeferredResult,
18
20
  DetectErrorBoundaryFunction,
@@ -20,8 +22,8 @@ import type {
20
22
  FormEncType,
21
23
  FormMethod,
22
24
  HTMLFormMethod,
25
+ HandlerResult,
23
26
  ImmutableRouteKey,
24
- LoaderFunction,
25
27
  MapRoutePropertiesFunction,
26
28
  MutationFormMethod,
27
29
  RedirectResult,
@@ -357,6 +359,7 @@ export interface FutureConfig {
357
359
  v7_partialHydration: boolean;
358
360
  v7_prependBasename: boolean;
359
361
  v7_relativeSplatPath: boolean;
362
+ unstable_skipActionErrorRevalidation: boolean;
360
363
  }
361
364
 
362
365
  /**
@@ -374,6 +377,7 @@ export interface RouterInit {
374
377
  future?: Partial<FutureConfig>;
375
378
  hydrationData?: HydrationState;
376
379
  window?: Window;
380
+ unstable_dataStrategy?: DataStrategyFunction;
377
381
  }
378
382
 
379
383
  /**
@@ -400,7 +404,13 @@ export interface StaticHandler {
400
404
  dataRoutes: AgnosticDataRouteObject[];
401
405
  query(
402
406
  request: Request,
403
- opts?: { requestContext?: unknown }
407
+ opts?: {
408
+ loadRouteIds?: string[];
409
+ requestContext?: unknown;
410
+ skipLoaders?: boolean;
411
+ skipLoaderErrorBubbling?: boolean;
412
+ unstable_dataStrategy?: DataStrategyFunction;
413
+ }
404
414
  ): Promise<StaticHandlerContext | Response>;
405
415
  queryRoute(
406
416
  request: Request,
@@ -616,18 +626,14 @@ interface ShortCircuitable {
616
626
  shortCircuited?: boolean;
617
627
  }
618
628
 
629
+ type PendingActionResult = [string, SuccessResult | ErrorResult];
630
+
619
631
  interface HandleActionResult extends ShortCircuitable {
620
632
  /**
621
- * Error thrown from the current action, keyed by the route containing the
622
- * error boundary to render the error. To be committed to the state after
623
- * loaders have completed
624
- */
625
- pendingActionError?: RouteData;
626
- /**
627
- * Data returned from the current action, keyed by the route owning the action.
628
- * To be committed to the state after loaders have completed
633
+ * Tuple for the returned or thrown value from the current action. The routeId
634
+ * is the action route for success and the bubbled boundary route for errors.
629
635
  */
630
- pendingActionData?: RouteData;
636
+ pendingActionResult?: PendingActionResult;
631
637
  }
632
638
 
633
639
  interface HandleLoadersResult extends ShortCircuitable {
@@ -660,16 +666,6 @@ interface RevalidatingFetcher extends FetchLoadMatch {
660
666
  controller: AbortController | null;
661
667
  }
662
668
 
663
- /**
664
- * Wrapper object to allow us to throw any response out from callLoaderOrAction
665
- * for queryRouter while preserving whether or not it was thrown or returned
666
- * from the loader/action
667
- */
668
- interface QueryRouteResponse {
669
- type: ResultType.data | ResultType.error;
670
- response: Response;
671
- }
672
-
673
669
  const validMutationMethodsArr: MutationFormMethod[] = [
674
670
  "post",
675
671
  "put",
@@ -776,6 +772,7 @@ export function createRouter(init: RouterInit): Router {
776
772
  );
777
773
  let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
778
774
  let basename = init.basename || "/";
775
+ let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
779
776
  // Config driven behavior flags
780
777
  let future: FutureConfig = {
781
778
  v7_fetcherPersist: false,
@@ -783,6 +780,7 @@ export function createRouter(init: RouterInit): Router {
783
780
  v7_partialHydration: false,
784
781
  v7_prependBasename: false,
785
782
  v7_relativeSplatPath: false,
783
+ unstable_skipActionErrorRevalidation: false,
786
784
  ...init.future,
787
785
  };
788
786
  // Cleanup function for history
@@ -835,9 +833,16 @@ export function createRouter(init: RouterInit): Router {
835
833
  let errors = init.hydrationData ? init.hydrationData.errors : null;
836
834
  let isRouteInitialized = (m: AgnosticDataRouteMatch) => {
837
835
  // No loader, nothing to initialize
838
- if (!m.route.loader) return true;
836
+ if (!m.route.loader) {
837
+ return true;
838
+ }
839
839
  // Explicitly opting-in to running on hydration
840
- if (m.route.loader.hydrate === true) return false;
840
+ if (
841
+ typeof m.route.loader === "function" &&
842
+ m.route.loader.hydrate === true
843
+ ) {
844
+ return false;
845
+ }
841
846
  // Otherwise, initialized if hydrated with data or an error
842
847
  return (
843
848
  (loaderData && loaderData[m.route.id] !== undefined) ||
@@ -1493,24 +1498,24 @@ export function createRouter(init: RouterInit): Router {
1493
1498
  pendingNavigationController.signal,
1494
1499
  opts && opts.submission
1495
1500
  );
1496
- let pendingActionData: RouteData | undefined;
1497
- let pendingError: RouteData | undefined;
1501
+ let pendingActionResult: PendingActionResult | undefined;
1498
1502
 
1499
1503
  if (opts && opts.pendingError) {
1500
1504
  // If we have a pendingError, it means the user attempted a GET submission
1501
1505
  // with binary FormData so assign here and skip to handleLoaders. That
1502
1506
  // way we handle calling loaders above the boundary etc. It's not really
1503
1507
  // different from an actionError in that sense.
1504
- pendingError = {
1505
- [findNearestBoundary(matches).route.id]: opts.pendingError,
1506
- };
1508
+ pendingActionResult = [
1509
+ findNearestBoundary(matches).route.id,
1510
+ { type: ResultType.error, error: opts.pendingError },
1511
+ ];
1507
1512
  } else if (
1508
1513
  opts &&
1509
1514
  opts.submission &&
1510
1515
  isMutationMethod(opts.submission.formMethod)
1511
1516
  ) {
1512
1517
  // Call action if we received an action submission
1513
- let actionOutput = await handleAction(
1518
+ let actionResult = await handleAction(
1514
1519
  request,
1515
1520
  location,
1516
1521
  opts.submission,
@@ -1518,17 +1523,20 @@ export function createRouter(init: RouterInit): Router {
1518
1523
  { replace: opts.replace, flushSync }
1519
1524
  );
1520
1525
 
1521
- if (actionOutput.shortCircuited) {
1526
+ if (actionResult.shortCircuited) {
1522
1527
  return;
1523
1528
  }
1524
1529
 
1525
- pendingActionData = actionOutput.pendingActionData;
1526
- pendingError = actionOutput.pendingActionError;
1530
+ pendingActionResult = actionResult.pendingActionResult;
1527
1531
  loadingNavigation = getLoadingNavigation(location, opts.submission);
1528
1532
  flushSync = false;
1529
1533
 
1530
1534
  // Create a GET request for the loaders
1531
- request = new Request(request.url, { signal: request.signal });
1535
+ request = createClientSideRequest(
1536
+ init.history,
1537
+ request.url,
1538
+ request.signal
1539
+ );
1532
1540
  }
1533
1541
 
1534
1542
  // Call loaders
@@ -1542,8 +1550,7 @@ export function createRouter(init: RouterInit): Router {
1542
1550
  opts && opts.replace,
1543
1551
  opts && opts.initialHydration === true,
1544
1552
  flushSync,
1545
- pendingActionData,
1546
- pendingError
1553
+ pendingActionResult
1547
1554
  );
1548
1555
 
1549
1556
  if (shortCircuited) {
@@ -1557,7 +1564,7 @@ export function createRouter(init: RouterInit): Router {
1557
1564
 
1558
1565
  completeNavigation(location, {
1559
1566
  matches,
1560
- ...(pendingActionData ? { actionData: pendingActionData } : {}),
1567
+ ...getActionDataForCommit(pendingActionResult),
1561
1568
  loaderData,
1562
1569
  errors,
1563
1570
  });
@@ -1592,16 +1599,13 @@ export function createRouter(init: RouterInit): Router {
1592
1599
  }),
1593
1600
  };
1594
1601
  } else {
1595
- result = await callLoaderOrAction(
1602
+ let results = await callDataStrategy(
1596
1603
  "action",
1597
1604
  request,
1598
- actionMatch,
1599
- matches,
1600
- manifest,
1601
- mapRouteProperties,
1602
- basename,
1603
- future.v7_relativeSplatPath
1605
+ [actionMatch],
1606
+ matches
1604
1607
  );
1608
+ result = results[0];
1605
1609
 
1606
1610
  if (request.signal.aborted) {
1607
1611
  return { shortCircuited: true };
@@ -1616,13 +1620,24 @@ export function createRouter(init: RouterInit): Router {
1616
1620
  // If the user didn't explicity indicate replace behavior, replace if
1617
1621
  // we redirected to the exact same location we're currently at to avoid
1618
1622
  // double back-buttons
1619
- replace =
1620
- result.location === state.location.pathname + state.location.search;
1623
+ let location = normalizeRedirectLocation(
1624
+ result.response.headers.get("Location")!,
1625
+ new URL(request.url),
1626
+ basename
1627
+ );
1628
+ replace = location === state.location.pathname + state.location.search;
1621
1629
  }
1622
- await startRedirectNavigation(state, result, { submission, replace });
1630
+ await startRedirectNavigation(request, result, {
1631
+ submission,
1632
+ replace,
1633
+ });
1623
1634
  return { shortCircuited: true };
1624
1635
  }
1625
1636
 
1637
+ if (isDeferredResult(result)) {
1638
+ throw getInternalRouterError(400, { type: "defer-action" });
1639
+ }
1640
+
1626
1641
  if (isErrorResult(result)) {
1627
1642
  // Store off the pending error - we use it to determine which loaders
1628
1643
  // to call and will commit it when we complete the navigation
@@ -1637,18 +1652,12 @@ export function createRouter(init: RouterInit): Router {
1637
1652
  }
1638
1653
 
1639
1654
  return {
1640
- // Send back an empty object we can use to clear out any prior actionData
1641
- pendingActionData: {},
1642
- pendingActionError: { [boundaryMatch.route.id]: result.error },
1655
+ pendingActionResult: [boundaryMatch.route.id, result],
1643
1656
  };
1644
1657
  }
1645
1658
 
1646
- if (isDeferredResult(result)) {
1647
- throw getInternalRouterError(400, { type: "defer-action" });
1648
- }
1649
-
1650
1659
  return {
1651
- pendingActionData: { [actionMatch.route.id]: result.data },
1660
+ pendingActionResult: [actionMatch.route.id, result],
1652
1661
  };
1653
1662
  }
1654
1663
 
@@ -1664,8 +1673,7 @@ export function createRouter(init: RouterInit): Router {
1664
1673
  replace?: boolean,
1665
1674
  initialHydration?: boolean,
1666
1675
  flushSync?: boolean,
1667
- pendingActionData?: RouteData,
1668
- pendingError?: RouteData
1676
+ pendingActionResult?: PendingActionResult
1669
1677
  ): Promise<HandleLoadersResult> {
1670
1678
  // Figure out the right navigation we want to use for data loading
1671
1679
  let loadingNavigation =
@@ -1686,6 +1694,7 @@ export function createRouter(init: RouterInit): Router {
1686
1694
  activeSubmission,
1687
1695
  location,
1688
1696
  future.v7_partialHydration && initialHydration === true,
1697
+ future.unstable_skipActionErrorRevalidation,
1689
1698
  isRevalidationRequired,
1690
1699
  cancelledDeferredRoutes,
1691
1700
  cancelledFetcherLoads,
@@ -1694,8 +1703,7 @@ export function createRouter(init: RouterInit): Router {
1694
1703
  fetchRedirectIds,
1695
1704
  routesToUse,
1696
1705
  basename,
1697
- pendingActionData,
1698
- pendingError
1706
+ pendingActionResult
1699
1707
  );
1700
1708
 
1701
1709
  // Cancel pending deferreds for no-longer-matched routes or routes we're
@@ -1718,8 +1726,11 @@ export function createRouter(init: RouterInit): Router {
1718
1726
  matches,
1719
1727
  loaderData: {},
1720
1728
  // Commit pending error if we're short circuiting
1721
- errors: pendingError || null,
1722
- ...(pendingActionData ? { actionData: pendingActionData } : {}),
1729
+ errors:
1730
+ pendingActionResult && isErrorResult(pendingActionResult[1])
1731
+ ? { [pendingActionResult[0]]: pendingActionResult[1].error }
1732
+ : null,
1733
+ ...getActionDataForCommit(pendingActionResult),
1723
1734
  ...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),
1724
1735
  },
1725
1736
  { flushSync }
@@ -1745,15 +1756,27 @@ export function createRouter(init: RouterInit): Router {
1745
1756
  );
1746
1757
  state.fetchers.set(rf.key, revalidatingFetcher);
1747
1758
  });
1748
- let actionData = pendingActionData || state.actionData;
1759
+
1760
+ let actionData: Record<string, RouteData> | null | undefined;
1761
+ if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
1762
+ // This is cast to `any` currently because `RouteData`uses any and it
1763
+ // would be a breaking change to use any.
1764
+ // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
1765
+ actionData = {
1766
+ [pendingActionResult[0]]: pendingActionResult[1].data as any,
1767
+ };
1768
+ } else if (state.actionData) {
1769
+ if (Object.keys(state.actionData).length === 0) {
1770
+ actionData = null;
1771
+ } else {
1772
+ actionData = state.actionData;
1773
+ }
1774
+ }
1775
+
1749
1776
  updateState(
1750
1777
  {
1751
1778
  navigation: loadingNavigation,
1752
- ...(actionData
1753
- ? Object.keys(actionData).length === 0
1754
- ? { actionData: null }
1755
- : { actionData }
1756
- : {}),
1779
+ ...(actionData !== undefined ? { actionData } : {}),
1757
1780
  ...(revalidatingFetchers.length > 0
1758
1781
  ? { fetchers: new Map(state.fetchers) }
1759
1782
  : {}),
@@ -1786,7 +1809,7 @@ export function createRouter(init: RouterInit): Router {
1786
1809
  );
1787
1810
  }
1788
1811
 
1789
- let { results, loaderResults, fetcherResults } =
1812
+ let { loaderResults, fetcherResults } =
1790
1813
  await callLoadersAndMaybeResolveData(
1791
1814
  state.matches,
1792
1815
  matches,
@@ -1811,7 +1834,7 @@ export function createRouter(init: RouterInit): Router {
1811
1834
  revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
1812
1835
 
1813
1836
  // If any loaders returned a redirect Response, start a new REPLACE navigation
1814
- let redirect = findRedirect(results);
1837
+ let redirect = findRedirect([...loaderResults, ...fetcherResults]);
1815
1838
  if (redirect) {
1816
1839
  if (redirect.idx >= matchesToLoad.length) {
1817
1840
  // If this redirect came from a fetcher make sure we mark it in
@@ -1821,7 +1844,9 @@ export function createRouter(init: RouterInit): Router {
1821
1844
  revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
1822
1845
  fetchRedirectIds.add(fetcherKey);
1823
1846
  }
1824
- await startRedirectNavigation(state, redirect.result, { replace });
1847
+ await startRedirectNavigation(request, redirect.result, {
1848
+ replace,
1849
+ });
1825
1850
  return { shortCircuited: true };
1826
1851
  }
1827
1852
 
@@ -1831,7 +1856,7 @@ export function createRouter(init: RouterInit): Router {
1831
1856
  matches,
1832
1857
  matchesToLoad,
1833
1858
  loaderResults,
1834
- pendingError,
1859
+ pendingActionResult,
1835
1860
  revalidatingFetchers,
1836
1861
  fetcherResults,
1837
1862
  activeDeferreds
@@ -1995,16 +2020,13 @@ export function createRouter(init: RouterInit): Router {
1995
2020
  fetchControllers.set(key, abortController);
1996
2021
 
1997
2022
  let originatingLoadId = incrementingLoadId;
1998
- let actionResult = await callLoaderOrAction(
2023
+ let actionResults = await callDataStrategy(
1999
2024
  "action",
2000
2025
  fetchRequest,
2001
- match,
2002
- requestMatches,
2003
- manifest,
2004
- mapRouteProperties,
2005
- basename,
2006
- future.v7_relativeSplatPath
2026
+ [match],
2027
+ requestMatches
2007
2028
  );
2029
+ let actionResult = actionResults[0];
2008
2030
 
2009
2031
  if (fetchRequest.signal.aborted) {
2010
2032
  // We can delete this so long as we weren't aborted by our own fetcher
@@ -2037,7 +2059,7 @@ export function createRouter(init: RouterInit): Router {
2037
2059
  } else {
2038
2060
  fetchRedirectIds.add(key);
2039
2061
  updateFetcherState(key, getLoadingFetcher(submission));
2040
- return startRedirectNavigation(state, actionResult, {
2062
+ return startRedirectNavigation(fetchRequest, actionResult, {
2041
2063
  fetcherSubmission: submission,
2042
2064
  });
2043
2065
  }
@@ -2083,6 +2105,7 @@ export function createRouter(init: RouterInit): Router {
2083
2105
  submission,
2084
2106
  nextLocation,
2085
2107
  false,
2108
+ future.unstable_skipActionErrorRevalidation,
2086
2109
  isRevalidationRequired,
2087
2110
  cancelledDeferredRoutes,
2088
2111
  cancelledFetcherLoads,
@@ -2091,8 +2114,7 @@ export function createRouter(init: RouterInit): Router {
2091
2114
  fetchRedirectIds,
2092
2115
  routesToUse,
2093
2116
  basename,
2094
- { [match.route.id]: actionResult.data },
2095
- undefined // No need to send through errors since we short circuit above
2117
+ [match.route.id, actionResult]
2096
2118
  );
2097
2119
 
2098
2120
  // Put all revalidating fetchers into the loading state, except for the
@@ -2126,7 +2148,7 @@ export function createRouter(init: RouterInit): Router {
2126
2148
  abortPendingFetchRevalidations
2127
2149
  );
2128
2150
 
2129
- let { results, loaderResults, fetcherResults } =
2151
+ let { loaderResults, fetcherResults } =
2130
2152
  await callLoadersAndMaybeResolveData(
2131
2153
  state.matches,
2132
2154
  matches,
@@ -2148,7 +2170,7 @@ export function createRouter(init: RouterInit): Router {
2148
2170
  fetchControllers.delete(key);
2149
2171
  revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
2150
2172
 
2151
- let redirect = findRedirect(results);
2173
+ let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2152
2174
  if (redirect) {
2153
2175
  if (redirect.idx >= matchesToLoad.length) {
2154
2176
  // If this redirect came from a fetcher make sure we mark it in
@@ -2158,7 +2180,7 @@ export function createRouter(init: RouterInit): Router {
2158
2180
  revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2159
2181
  fetchRedirectIds.add(fetcherKey);
2160
2182
  }
2161
- return startRedirectNavigation(state, redirect.result);
2183
+ return startRedirectNavigation(revalidationRequest, redirect.result);
2162
2184
  }
2163
2185
 
2164
2186
  // Process and commit output from loaders
@@ -2246,16 +2268,13 @@ export function createRouter(init: RouterInit): Router {
2246
2268
  fetchControllers.set(key, abortController);
2247
2269
 
2248
2270
  let originatingLoadId = incrementingLoadId;
2249
- let result: DataResult = await callLoaderOrAction(
2271
+ let results = await callDataStrategy(
2250
2272
  "loader",
2251
2273
  fetchRequest,
2252
- match,
2253
- matches,
2254
- manifest,
2255
- mapRouteProperties,
2256
- basename,
2257
- future.v7_relativeSplatPath
2274
+ [match],
2275
+ matches
2258
2276
  );
2277
+ let result = results[0];
2259
2278
 
2260
2279
  // Deferred isn't supported for fetcher loads, await everything and treat it
2261
2280
  // as a normal load. resolveDeferredData will return undefined if this
@@ -2293,7 +2312,7 @@ export function createRouter(init: RouterInit): Router {
2293
2312
  return;
2294
2313
  } else {
2295
2314
  fetchRedirectIds.add(key);
2296
- await startRedirectNavigation(state, result);
2315
+ await startRedirectNavigation(fetchRequest, result);
2297
2316
  return;
2298
2317
  }
2299
2318
  }
@@ -2330,7 +2349,7 @@ export function createRouter(init: RouterInit): Router {
2330
2349
  * the history action from the original navigation (PUSH or REPLACE).
2331
2350
  */
2332
2351
  async function startRedirectNavigation(
2333
- state: RouterState,
2352
+ request: Request,
2334
2353
  redirect: RedirectResult,
2335
2354
  {
2336
2355
  submission,
@@ -2342,26 +2361,29 @@ export function createRouter(init: RouterInit): Router {
2342
2361
  replace?: boolean;
2343
2362
  } = {}
2344
2363
  ) {
2345
- if (redirect.revalidate) {
2364
+ if (redirect.response.headers.has("X-Remix-Revalidate")) {
2346
2365
  isRevalidationRequired = true;
2347
2366
  }
2348
2367
 
2349
- let redirectLocation = createLocation(state.location, redirect.location, {
2368
+ let location = redirect.response.headers.get("Location");
2369
+ invariant(location, "Expected a Location header on the redirect Response");
2370
+ location = normalizeRedirectLocation(
2371
+ location,
2372
+ new URL(request.url),
2373
+ basename
2374
+ );
2375
+ let redirectLocation = createLocation(state.location, location, {
2350
2376
  _isRedirect: true,
2351
2377
  });
2352
- invariant(
2353
- redirectLocation,
2354
- "Expected a location on the redirect navigation"
2355
- );
2356
2378
 
2357
2379
  if (isBrowser) {
2358
2380
  let isDocumentReload = false;
2359
2381
 
2360
- if (redirect.reloadDocument) {
2382
+ if (redirect.response.headers.has("X-Remix-Reload-Document")) {
2361
2383
  // Hard reload if the response contained X-Remix-Reload-Document
2362
2384
  isDocumentReload = true;
2363
- } else if (ABSOLUTE_URL_REGEX.test(redirect.location)) {
2364
- const url = init.history.createURL(redirect.location);
2385
+ } else if (ABSOLUTE_URL_REGEX.test(location)) {
2386
+ const url = init.history.createURL(location);
2365
2387
  isDocumentReload =
2366
2388
  // Hard reload if it's an absolute URL to a new origin
2367
2389
  url.origin !== routerWindow.location.origin ||
@@ -2371,9 +2393,9 @@ export function createRouter(init: RouterInit): Router {
2371
2393
 
2372
2394
  if (isDocumentReload) {
2373
2395
  if (replace) {
2374
- routerWindow.location.replace(redirect.location);
2396
+ routerWindow.location.replace(location);
2375
2397
  } else {
2376
- routerWindow.location.assign(redirect.location);
2398
+ routerWindow.location.assign(location);
2377
2399
  }
2378
2400
  return;
2379
2401
  }
@@ -2404,14 +2426,14 @@ export function createRouter(init: RouterInit): Router {
2404
2426
  // redirected location
2405
2427
  let activeSubmission = submission || fetcherSubmission;
2406
2428
  if (
2407
- redirectPreserveMethodStatusCodes.has(redirect.status) &&
2429
+ redirectPreserveMethodStatusCodes.has(redirect.response.status) &&
2408
2430
  activeSubmission &&
2409
2431
  isMutationMethod(activeSubmission.formMethod)
2410
2432
  ) {
2411
2433
  await startNavigation(redirectHistoryAction, redirectLocation, {
2412
2434
  submission: {
2413
2435
  ...activeSubmission,
2414
- formAction: redirect.location,
2436
+ formAction: location,
2415
2437
  },
2416
2438
  // Preserve this flag across redirects
2417
2439
  preventScrollReset: pendingPreventScrollReset,
@@ -2433,6 +2455,55 @@ export function createRouter(init: RouterInit): Router {
2433
2455
  }
2434
2456
  }
2435
2457
 
2458
+ // Utility wrapper for calling dataStrategy client-side without having to
2459
+ // pass around the manifest, mapRouteProperties, etc.
2460
+ async function callDataStrategy(
2461
+ type: "loader" | "action",
2462
+ request: Request,
2463
+ matchesToLoad: AgnosticDataRouteMatch[],
2464
+ matches: AgnosticDataRouteMatch[]
2465
+ ): Promise<DataResult[]> {
2466
+ try {
2467
+ let results = await callDataStrategyImpl(
2468
+ dataStrategyImpl,
2469
+ type,
2470
+ request,
2471
+ matchesToLoad,
2472
+ matches,
2473
+ manifest,
2474
+ mapRouteProperties
2475
+ );
2476
+
2477
+ return await Promise.all(
2478
+ results.map((result, i) => {
2479
+ if (isRedirectHandlerResult(result)) {
2480
+ let response = result.result as Response;
2481
+ return {
2482
+ type: ResultType.redirect,
2483
+ response: normalizeRelativeRoutingRedirectResponse(
2484
+ response,
2485
+ request,
2486
+ matchesToLoad[i].route.id,
2487
+ matches,
2488
+ basename,
2489
+ future.v7_relativeSplatPath
2490
+ ),
2491
+ };
2492
+ }
2493
+
2494
+ return convertHandlerResultToDataResult(result);
2495
+ })
2496
+ );
2497
+ } catch (e) {
2498
+ // If the outer dataStrategy method throws, just return the error for all
2499
+ // matches - and it'll naturally bubble to the root
2500
+ return matchesToLoad.map(() => ({
2501
+ type: ResultType.error,
2502
+ error: e,
2503
+ }));
2504
+ }
2505
+ }
2506
+
2436
2507
  async function callLoadersAndMaybeResolveData(
2437
2508
  currentMatches: AgnosticDataRouteMatch[],
2438
2509
  matches: AgnosticDataRouteMatch[],
@@ -2440,45 +2511,33 @@ export function createRouter(init: RouterInit): Router {
2440
2511
  fetchersToLoad: RevalidatingFetcher[],
2441
2512
  request: Request
2442
2513
  ) {
2443
- // Call all navigation loaders and revalidating fetcher loaders in parallel,
2444
- // then slice off the results into separate arrays so we can handle them
2445
- // accordingly
2446
- let results = await Promise.all([
2447
- ...matchesToLoad.map((match) =>
2448
- callLoaderOrAction(
2449
- "loader",
2450
- request,
2451
- match,
2452
- matches,
2453
- manifest,
2454
- mapRouteProperties,
2455
- basename,
2456
- future.v7_relativeSplatPath
2457
- )
2458
- ),
2514
+ let [loaderResults, ...fetcherResults] = await Promise.all([
2515
+ matchesToLoad.length
2516
+ ? callDataStrategy("loader", request, matchesToLoad, matches)
2517
+ : [],
2459
2518
  ...fetchersToLoad.map((f) => {
2460
2519
  if (f.matches && f.match && f.controller) {
2461
- return callLoaderOrAction(
2462
- "loader",
2463
- createClientSideRequest(init.history, f.path, f.controller.signal),
2464
- f.match,
2465
- f.matches,
2466
- manifest,
2467
- mapRouteProperties,
2468
- basename,
2469
- future.v7_relativeSplatPath
2520
+ let fetcherRequest = createClientSideRequest(
2521
+ init.history,
2522
+ f.path,
2523
+ f.controller.signal
2470
2524
  );
2525
+ return callDataStrategy(
2526
+ "loader",
2527
+ fetcherRequest,
2528
+ [f.match],
2529
+ f.matches
2530
+ ).then((r) => r[0]);
2471
2531
  } else {
2472
- let error: ErrorResult = {
2532
+ return Promise.resolve<DataResult>({
2473
2533
  type: ResultType.error,
2474
- error: getInternalRouterError(404, { pathname: f.path }),
2475
- };
2476
- return error;
2534
+ error: getInternalRouterError(404, {
2535
+ pathname: f.path,
2536
+ }),
2537
+ });
2477
2538
  }
2478
2539
  }),
2479
2540
  ]);
2480
- let loaderResults = results.slice(0, matchesToLoad.length);
2481
- let fetcherResults = results.slice(matchesToLoad.length);
2482
2541
 
2483
2542
  await Promise.all([
2484
2543
  resolveDeferredResults(
@@ -2498,7 +2557,10 @@ export function createRouter(init: RouterInit): Router {
2498
2557
  ),
2499
2558
  ]);
2500
2559
 
2501
- return { results, loaderResults, fetcherResults };
2560
+ return {
2561
+ loaderResults,
2562
+ fetcherResults,
2563
+ };
2502
2564
  }
2503
2565
 
2504
2566
  function interruptActiveLoads() {
@@ -2925,10 +2987,33 @@ export function createStaticHandler(
2925
2987
  * redirect response is returned or thrown from any action/loader. We
2926
2988
  * propagate that out and return the raw Response so the HTTP server can
2927
2989
  * return it directly.
2990
+ *
2991
+ * - `opts.loadRouteIds` is an optional array of routeIds to run only a subset of
2992
+ * loaders during a query() call
2993
+ * - `opts.requestContext` is an optional server context that will be passed
2994
+ * to actions/loaders in the `context` parameter
2995
+ * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
2996
+ * the bubbling of errors which allows single-fetch-type implementations
2997
+ * where the client will handle the bubbling and we may need to return data
2998
+ * for the handling route
2999
+ * - `opts.skipLoaders` is an optional parameter that will prevent loaders
3000
+ * from running after an action
2928
3001
  */
2929
3002
  async function query(
2930
3003
  request: Request,
2931
- { requestContext }: { requestContext?: unknown } = {}
3004
+ {
3005
+ loadRouteIds,
3006
+ requestContext,
3007
+ skipLoaderErrorBubbling,
3008
+ skipLoaders,
3009
+ unstable_dataStrategy,
3010
+ }: {
3011
+ loadRouteIds?: string[];
3012
+ requestContext?: unknown;
3013
+ skipLoaderErrorBubbling?: boolean;
3014
+ skipLoaders?: boolean;
3015
+ unstable_dataStrategy?: DataStrategyFunction;
3016
+ } = {}
2932
3017
  ): Promise<StaticHandlerContext | Response> {
2933
3018
  let url = new URL(request.url);
2934
3019
  let method = request.method;
@@ -2974,7 +3059,17 @@ export function createStaticHandler(
2974
3059
  };
2975
3060
  }
2976
3061
 
2977
- let result = await queryImpl(request, location, matches, requestContext);
3062
+ let result = await queryImpl(
3063
+ request,
3064
+ location,
3065
+ matches,
3066
+ requestContext,
3067
+ unstable_dataStrategy || null,
3068
+ loadRouteIds || null,
3069
+ skipLoaderErrorBubbling === true,
3070
+ skipLoaders === true,
3071
+ null
3072
+ );
2978
3073
  if (isResponse(result)) {
2979
3074
  return result;
2980
3075
  }
@@ -3004,6 +3099,12 @@ export function createStaticHandler(
3004
3099
  * serialize the error as they see fit while including the proper response
3005
3100
  * code. Examples here are 404 and 405 errors that occur prior to reaching
3006
3101
  * any user-defined loaders.
3102
+ *
3103
+ * - `opts.routeId` allows you to specify the specific route handler to call.
3104
+ * If not provided the handler will determine the proper route by matching
3105
+ * against `request.url`
3106
+ * - `opts.requestContext` is an optional server context that will be passed
3107
+ * to actions/loaders in the `context` parameter
3007
3108
  */
3008
3109
  async function queryRoute(
3009
3110
  request: Request,
@@ -3043,6 +3144,10 @@ export function createStaticHandler(
3043
3144
  location,
3044
3145
  matches,
3045
3146
  requestContext,
3147
+ null,
3148
+ null,
3149
+ false,
3150
+ false,
3046
3151
  match
3047
3152
  );
3048
3153
  if (isResponse(result)) {
@@ -3079,7 +3184,11 @@ export function createStaticHandler(
3079
3184
  location: Location,
3080
3185
  matches: AgnosticDataRouteMatch[],
3081
3186
  requestContext: unknown,
3082
- routeMatch?: AgnosticDataRouteMatch
3187
+ unstable_dataStrategy: DataStrategyFunction | null,
3188
+ loadRouteIds: string[] | null,
3189
+ skipLoaderErrorBubbling: boolean,
3190
+ skipLoaders: boolean,
3191
+ routeMatch: AgnosticDataRouteMatch | null
3083
3192
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
3084
3193
  invariant(
3085
3194
  request.signal,
@@ -3093,6 +3202,10 @@ export function createStaticHandler(
3093
3202
  matches,
3094
3203
  routeMatch || getTargetMatch(matches, location),
3095
3204
  requestContext,
3205
+ unstable_dataStrategy,
3206
+ loadRouteIds,
3207
+ skipLoaderErrorBubbling,
3208
+ skipLoaders,
3096
3209
  routeMatch != null
3097
3210
  );
3098
3211
  return result;
@@ -3102,6 +3215,9 @@ export function createStaticHandler(
3102
3215
  request,
3103
3216
  matches,
3104
3217
  requestContext,
3218
+ unstable_dataStrategy,
3219
+ loadRouteIds,
3220
+ skipLoaderErrorBubbling,
3105
3221
  routeMatch
3106
3222
  );
3107
3223
  return isResponse(result)
@@ -3112,14 +3228,14 @@ export function createStaticHandler(
3112
3228
  actionHeaders: {},
3113
3229
  };
3114
3230
  } catch (e) {
3115
- // If the user threw/returned a Response in callLoaderOrAction, we throw
3116
- // it to bail out and then return or throw here based on whether the user
3117
- // returned or threw
3118
- if (isQueryRouteResponse(e)) {
3231
+ // If the user threw/returned a Response in callLoaderOrAction for a
3232
+ // `queryRoute` call, we throw the `HandlerResult` to bail out early
3233
+ // and then return or throw the raw Response here accordingly
3234
+ if (isHandlerResult(e) && isResponse(e.result)) {
3119
3235
  if (e.type === ResultType.error) {
3120
- throw e.response;
3236
+ throw e.result;
3121
3237
  }
3122
- return e.response;
3238
+ return e.result;
3123
3239
  }
3124
3240
  // Redirects are always returned since they don't propagate to catch
3125
3241
  // boundaries
@@ -3135,6 +3251,10 @@ export function createStaticHandler(
3135
3251
  matches: AgnosticDataRouteMatch[],
3136
3252
  actionMatch: AgnosticDataRouteMatch,
3137
3253
  requestContext: unknown,
3254
+ unstable_dataStrategy: DataStrategyFunction | null,
3255
+ loadRouteIds: string[] | null,
3256
+ skipLoaderErrorBubbling: boolean,
3257
+ skipLoaders: boolean,
3138
3258
  isRouteRequest: boolean
3139
3259
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
3140
3260
  let result: DataResult;
@@ -3153,17 +3273,16 @@ export function createStaticHandler(
3153
3273
  error,
3154
3274
  };
3155
3275
  } else {
3156
- result = await callLoaderOrAction(
3276
+ let results = await callDataStrategy(
3157
3277
  "action",
3158
3278
  request,
3159
- actionMatch,
3279
+ [actionMatch],
3160
3280
  matches,
3161
- manifest,
3162
- mapRouteProperties,
3163
- basename,
3164
- future.v7_relativeSplatPath,
3165
- { isStaticRequest: true, isRouteRequest, requestContext }
3281
+ isRouteRequest,
3282
+ requestContext,
3283
+ unstable_dataStrategy
3166
3284
  );
3285
+ result = results[0];
3167
3286
 
3168
3287
  if (request.signal.aborted) {
3169
3288
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
@@ -3176,9 +3295,9 @@ export function createStaticHandler(
3176
3295
  // can get back on the "throw all redirect responses" train here should
3177
3296
  // this ever happen :/
3178
3297
  throw new Response(null, {
3179
- status: result.status,
3298
+ status: result.response.status,
3180
3299
  headers: {
3181
- Location: result.location,
3300
+ Location: result.response.headers.get("Location")!,
3182
3301
  },
3183
3302
  });
3184
3303
  }
@@ -3215,51 +3334,102 @@ export function createStaticHandler(
3215
3334
  };
3216
3335
  }
3217
3336
 
3337
+ // Create a GET request for the loaders
3338
+ let loaderRequest = new Request(request.url, {
3339
+ headers: request.headers,
3340
+ redirect: request.redirect,
3341
+ signal: request.signal,
3342
+ });
3343
+
3218
3344
  if (isErrorResult(result)) {
3219
3345
  // Store off the pending error - we use it to determine which loaders
3220
3346
  // to call and will commit it when we complete the navigation
3221
- let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
3347
+ let boundaryMatch = skipLoaderErrorBubbling
3348
+ ? actionMatch
3349
+ : findNearestBoundary(matches, actionMatch.route.id);
3350
+ let statusCode = isRouteErrorResponse(result.error)
3351
+ ? result.error.status
3352
+ : result.statusCode != null
3353
+ ? result.statusCode
3354
+ : 500;
3355
+ let actionHeaders = {
3356
+ ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
3357
+ };
3358
+
3359
+ if (skipLoaders) {
3360
+ return {
3361
+ matches,
3362
+ loaderData: {},
3363
+ actionData: {},
3364
+ errors: {
3365
+ [boundaryMatch.route.id]: result.error,
3366
+ },
3367
+ statusCode,
3368
+ loaderHeaders: {},
3369
+ actionHeaders,
3370
+ activeDeferreds: null,
3371
+ };
3372
+ }
3373
+
3222
3374
  let context = await loadRouteData(
3223
- request,
3375
+ loaderRequest,
3224
3376
  matches,
3225
3377
  requestContext,
3226
- undefined,
3227
- {
3228
- [boundaryMatch.route.id]: result.error,
3229
- }
3378
+ unstable_dataStrategy,
3379
+ loadRouteIds,
3380
+ skipLoaderErrorBubbling,
3381
+ null,
3382
+ [boundaryMatch.route.id, result]
3230
3383
  );
3231
3384
 
3232
3385
  // action status codes take precedence over loader status codes
3233
3386
  return {
3234
3387
  ...context,
3235
- statusCode: isRouteErrorResponse(result.error)
3236
- ? result.error.status
3237
- : 500,
3388
+ statusCode,
3238
3389
  actionData: null,
3239
- actionHeaders: {
3240
- ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
3390
+ actionHeaders,
3391
+ };
3392
+ }
3393
+
3394
+ let actionHeaders = result.headers
3395
+ ? { [actionMatch.route.id]: result.headers }
3396
+ : {};
3397
+
3398
+ if (skipLoaders) {
3399
+ return {
3400
+ matches,
3401
+ loaderData: {},
3402
+ actionData: {
3403
+ [actionMatch.route.id]: result.data,
3241
3404
  },
3405
+ errors: null,
3406
+ statusCode: result.statusCode || 200,
3407
+ loaderHeaders: {},
3408
+ actionHeaders,
3409
+ activeDeferreds: null,
3242
3410
  };
3243
3411
  }
3244
3412
 
3245
- // Create a GET request for the loaders
3246
- let loaderRequest = new Request(request.url, {
3247
- headers: request.headers,
3248
- redirect: request.redirect,
3249
- signal: request.signal,
3250
- });
3251
- let context = await loadRouteData(loaderRequest, matches, requestContext);
3413
+ let context = await loadRouteData(
3414
+ loaderRequest,
3415
+ matches,
3416
+ requestContext,
3417
+ unstable_dataStrategy,
3418
+ loadRouteIds,
3419
+ skipLoaderErrorBubbling,
3420
+ null
3421
+ );
3252
3422
 
3253
3423
  return {
3254
3424
  ...context,
3255
- // action status codes take precedence over loader status codes
3256
- ...(result.statusCode ? { statusCode: result.statusCode } : {}),
3257
3425
  actionData: {
3258
3426
  [actionMatch.route.id]: result.data,
3259
3427
  },
3260
- actionHeaders: {
3261
- ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
3262
- },
3428
+ // action status codes take precedence over loader status codes
3429
+ ...(result.statusCode ? { statusCode: result.statusCode } : {}),
3430
+ actionHeaders: result.headers
3431
+ ? { [actionMatch.route.id]: result.headers }
3432
+ : {},
3263
3433
  };
3264
3434
  }
3265
3435
 
@@ -3267,8 +3437,11 @@ export function createStaticHandler(
3267
3437
  request: Request,
3268
3438
  matches: AgnosticDataRouteMatch[],
3269
3439
  requestContext: unknown,
3270
- routeMatch?: AgnosticDataRouteMatch,
3271
- pendingActionError?: RouteData
3440
+ unstable_dataStrategy: DataStrategyFunction | null,
3441
+ loadRouteIds: string[] | null,
3442
+ skipLoaderErrorBubbling: boolean,
3443
+ routeMatch: AgnosticDataRouteMatch | null,
3444
+ pendingActionResult?: PendingActionResult
3272
3445
  ): Promise<
3273
3446
  | Omit<
3274
3447
  StaticHandlerContext,
@@ -3293,14 +3466,19 @@ export function createStaticHandler(
3293
3466
 
3294
3467
  let requestMatches = routeMatch
3295
3468
  ? [routeMatch]
3296
- : getLoaderMatchesUntilBoundary(
3297
- matches,
3298
- Object.keys(pendingActionError || {})[0]
3299
- );
3469
+ : pendingActionResult && isErrorResult(pendingActionResult[1])
3470
+ ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0])
3471
+ : matches;
3300
3472
  let matchesToLoad = requestMatches.filter(
3301
3473
  (m) => m.route.loader || m.route.lazy
3302
3474
  );
3303
3475
 
3476
+ if (loadRouteIds) {
3477
+ matchesToLoad = matchesToLoad.filter((m) =>
3478
+ loadRouteIds.includes(m.route.id)
3479
+ );
3480
+ }
3481
+
3304
3482
  // Short circuit if we have no loaders to run (query())
3305
3483
  if (matchesToLoad.length === 0) {
3306
3484
  return {
@@ -3310,28 +3488,27 @@ export function createStaticHandler(
3310
3488
  (acc, m) => Object.assign(acc, { [m.route.id]: null }),
3311
3489
  {}
3312
3490
  ),
3313
- errors: pendingActionError || null,
3491
+ errors:
3492
+ pendingActionResult && isErrorResult(pendingActionResult[1])
3493
+ ? {
3494
+ [pendingActionResult[0]]: pendingActionResult[1].error,
3495
+ }
3496
+ : null,
3314
3497
  statusCode: 200,
3315
3498
  loaderHeaders: {},
3316
3499
  activeDeferreds: null,
3317
3500
  };
3318
3501
  }
3319
3502
 
3320
- let results = await Promise.all([
3321
- ...matchesToLoad.map((match) =>
3322
- callLoaderOrAction(
3323
- "loader",
3324
- request,
3325
- match,
3326
- matches,
3327
- manifest,
3328
- mapRouteProperties,
3329
- basename,
3330
- future.v7_relativeSplatPath,
3331
- { isStaticRequest: true, isRouteRequest, requestContext }
3332
- )
3333
- ),
3334
- ]);
3503
+ let results = await callDataStrategy(
3504
+ "loader",
3505
+ request,
3506
+ matchesToLoad,
3507
+ matches,
3508
+ isRouteRequest,
3509
+ requestContext,
3510
+ unstable_dataStrategy
3511
+ );
3335
3512
 
3336
3513
  if (request.signal.aborted) {
3337
3514
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
@@ -3343,8 +3520,9 @@ export function createStaticHandler(
3343
3520
  matches,
3344
3521
  matchesToLoad,
3345
3522
  results,
3346
- pendingActionError,
3347
- activeDeferreds
3523
+ pendingActionResult,
3524
+ activeDeferreds,
3525
+ skipLoaderErrorBubbling
3348
3526
  );
3349
3527
 
3350
3528
  // Add a null for any non-loader matches for proper revalidation on the client
@@ -3367,6 +3545,53 @@ export function createStaticHandler(
3367
3545
  };
3368
3546
  }
3369
3547
 
3548
+ // Utility wrapper for calling dataStrategy server-side without having to
3549
+ // pass around the manifest, mapRouteProperties, etc.
3550
+ async function callDataStrategy(
3551
+ type: "loader" | "action",
3552
+ request: Request,
3553
+ matchesToLoad: AgnosticDataRouteMatch[],
3554
+ matches: AgnosticDataRouteMatch[],
3555
+ isRouteRequest: boolean,
3556
+ requestContext: unknown,
3557
+ unstable_dataStrategy: DataStrategyFunction | null
3558
+ ): Promise<DataResult[]> {
3559
+ let results = await callDataStrategyImpl(
3560
+ unstable_dataStrategy || defaultDataStrategy,
3561
+ type,
3562
+ request,
3563
+ matchesToLoad,
3564
+ matches,
3565
+ manifest,
3566
+ mapRouteProperties,
3567
+ requestContext
3568
+ );
3569
+
3570
+ return await Promise.all(
3571
+ results.map((result, i) => {
3572
+ if (isRedirectHandlerResult(result)) {
3573
+ let response = result.result as Response;
3574
+ // Throw redirects and let the server handle them with an HTTP redirect
3575
+ throw normalizeRelativeRoutingRedirectResponse(
3576
+ response,
3577
+ request,
3578
+ matchesToLoad[i].route.id,
3579
+ matches,
3580
+ basename,
3581
+ future.v7_relativeSplatPath
3582
+ );
3583
+ }
3584
+ if (isResponse(result.result) && isRouteRequest) {
3585
+ // For SSR single-route requests, we want to hand Responses back
3586
+ // directly without unwrapping
3587
+ throw result;
3588
+ }
3589
+
3590
+ return convertHandlerResultToDataResult(result);
3591
+ })
3592
+ );
3593
+ }
3594
+
3370
3595
  return {
3371
3596
  dataRoutes,
3372
3597
  query,
@@ -3643,7 +3868,7 @@ function normalizeNavigateOptions(
3643
3868
  // render so we don't need to load them
3644
3869
  function getLoaderMatchesUntilBoundary(
3645
3870
  matches: AgnosticDataRouteMatch[],
3646
- boundaryId?: string
3871
+ boundaryId: string
3647
3872
  ) {
3648
3873
  let boundaryMatches = matches;
3649
3874
  if (boundaryId) {
@@ -3662,6 +3887,7 @@ function getMatchesToLoad(
3662
3887
  submission: Submission | undefined,
3663
3888
  location: Location,
3664
3889
  isInitialLoad: boolean,
3890
+ skipActionErrorRevalidation: boolean,
3665
3891
  isRevalidationRequired: boolean,
3666
3892
  cancelledDeferredRoutes: string[],
3667
3893
  cancelledFetcherLoads: string[],
@@ -3670,21 +3896,33 @@ function getMatchesToLoad(
3670
3896
  fetchRedirectIds: Set<string>,
3671
3897
  routesToUse: AgnosticDataRouteObject[],
3672
3898
  basename: string | undefined,
3673
- pendingActionData?: RouteData,
3674
- pendingError?: RouteData
3899
+ pendingActionResult?: PendingActionResult
3675
3900
  ): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
3676
- let actionResult = pendingError
3677
- ? Object.values(pendingError)[0]
3678
- : pendingActionData
3679
- ? Object.values(pendingActionData)[0]
3901
+ let actionResult = pendingActionResult
3902
+ ? isErrorResult(pendingActionResult[1])
3903
+ ? pendingActionResult[1].error
3904
+ : pendingActionResult[1].data
3680
3905
  : undefined;
3681
-
3682
3906
  let currentUrl = history.createURL(state.location);
3683
3907
  let nextUrl = history.createURL(location);
3684
3908
 
3685
3909
  // Pick navigation matches that are net-new or qualify for revalidation
3686
- let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3687
- let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3910
+ let boundaryId =
3911
+ pendingActionResult && isErrorResult(pendingActionResult[1])
3912
+ ? pendingActionResult[0]
3913
+ : undefined;
3914
+ let boundaryMatches = boundaryId
3915
+ ? getLoaderMatchesUntilBoundary(matches, boundaryId)
3916
+ : matches;
3917
+
3918
+ // Don't revalidate loaders by default after action 4xx/5xx responses
3919
+ // when the flag is enabled. They can still opt-into revalidation via
3920
+ // `shouldRevalidate` via `actionResult`
3921
+ let actionStatus = pendingActionResult
3922
+ ? pendingActionResult[1].statusCode
3923
+ : undefined;
3924
+ let shouldSkipRevalidation =
3925
+ skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
3688
3926
 
3689
3927
  let navigationMatches = boundaryMatches.filter((match, index) => {
3690
3928
  let { route } = match;
@@ -3698,7 +3936,7 @@ function getMatchesToLoad(
3698
3936
  }
3699
3937
 
3700
3938
  if (isInitialLoad) {
3701
- if (route.loader.hydrate) {
3939
+ if (typeof route.loader !== "function" || route.loader.hydrate) {
3702
3940
  return true;
3703
3941
  }
3704
3942
  return (
@@ -3730,15 +3968,16 @@ function getMatchesToLoad(
3730
3968
  nextParams: nextRouteMatch.params,
3731
3969
  ...submission,
3732
3970
  actionResult,
3733
- defaultShouldRevalidate:
3734
- // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3735
- isRevalidationRequired ||
3736
- // Clicked the same link, resubmitted a GET form
3737
- currentUrl.pathname + currentUrl.search ===
3738
- nextUrl.pathname + nextUrl.search ||
3739
- // Search params affect all loaders
3740
- currentUrl.search !== nextUrl.search ||
3741
- isNewRouteInstance(currentRouteMatch, nextRouteMatch),
3971
+ unstable_actionStatus: actionStatus,
3972
+ defaultShouldRevalidate: shouldSkipRevalidation
3973
+ ? false
3974
+ : // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3975
+ isRevalidationRequired ||
3976
+ currentUrl.pathname + currentUrl.search ===
3977
+ nextUrl.pathname + nextUrl.search ||
3978
+ // Search params affect all loaders
3979
+ currentUrl.search !== nextUrl.search ||
3980
+ isNewRouteInstance(currentRouteMatch, nextRouteMatch),
3742
3981
  });
3743
3982
  });
3744
3983
 
@@ -3808,7 +4047,10 @@ function getMatchesToLoad(
3808
4047
  nextParams: matches[matches.length - 1].params,
3809
4048
  ...submission,
3810
4049
  actionResult,
3811
- defaultShouldRevalidate: isRevalidationRequired,
4050
+ unstable_actionStatus: actionStatus,
4051
+ defaultShouldRevalidate: shouldSkipRevalidation
4052
+ ? false
4053
+ : isRevalidationRequired,
3812
4054
  });
3813
4055
  }
3814
4056
 
@@ -3954,39 +4196,138 @@ async function loadLazyRouteModule(
3954
4196
  });
3955
4197
  }
3956
4198
 
4199
+ // Default implementation of `dataStrategy` which fetches all loaders in parallel
4200
+ function defaultDataStrategy(
4201
+ opts: DataStrategyFunctionArgs
4202
+ ): ReturnType<DataStrategyFunction> {
4203
+ return Promise.all(opts.matches.map((m) => m.resolve()));
4204
+ }
4205
+
4206
+ async function callDataStrategyImpl(
4207
+ dataStrategyImpl: DataStrategyFunction,
4208
+ type: "loader" | "action",
4209
+ request: Request,
4210
+ matchesToLoad: AgnosticDataRouteMatch[],
4211
+ matches: AgnosticDataRouteMatch[],
4212
+ manifest: RouteManifest,
4213
+ mapRouteProperties: MapRoutePropertiesFunction,
4214
+ requestContext?: unknown
4215
+ ): Promise<HandlerResult[]> {
4216
+ let routeIdsToLoad = matchesToLoad.reduce(
4217
+ (acc, m) => acc.add(m.route.id),
4218
+ new Set<string>()
4219
+ );
4220
+ let loadedMatches = new Set<string>();
4221
+
4222
+ // Send all matches here to allow for a middleware-type implementation.
4223
+ // handler will be a no-op for unneeded routes and we filter those results
4224
+ // back out below.
4225
+ let results = await dataStrategyImpl({
4226
+ matches: matches.map((match) => {
4227
+ let shouldLoad = routeIdsToLoad.has(match.route.id);
4228
+ // `resolve` encapsulates the route.lazy, executing the
4229
+ // loader/action, and mapping return values/thrown errors to a
4230
+ // HandlerResult. Users can pass a callback to take fine-grained control
4231
+ // over the execution of the loader/action
4232
+ let resolve: DataStrategyMatch["resolve"] = (handlerOverride) => {
4233
+ loadedMatches.add(match.route.id);
4234
+ return shouldLoad
4235
+ ? callLoaderOrAction(
4236
+ type,
4237
+ request,
4238
+ match,
4239
+ manifest,
4240
+ mapRouteProperties,
4241
+ handlerOverride,
4242
+ requestContext
4243
+ )
4244
+ : Promise.resolve({ type: ResultType.data, result: undefined });
4245
+ };
4246
+
4247
+ return {
4248
+ ...match,
4249
+ shouldLoad,
4250
+ resolve,
4251
+ };
4252
+ }),
4253
+ request,
4254
+ params: matches[0].params,
4255
+ context: requestContext,
4256
+ });
4257
+
4258
+ // Throw if any loadRoute implementations not called since they are what
4259
+ // ensures a route is fully loaded
4260
+ matches.forEach((m) =>
4261
+ invariant(
4262
+ loadedMatches.has(m.route.id),
4263
+ `\`match.resolve()\` was not called for route id "${m.route.id}". ` +
4264
+ "You must call `match.resolve()` on every match passed to " +
4265
+ "`dataStrategy` to ensure all routes are properly loaded."
4266
+ )
4267
+ );
4268
+
4269
+ // Filter out any middleware-only matches for which we didn't need to run handlers
4270
+ return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
4271
+ }
4272
+
4273
+ // Default logic for calling a loader/action is the user has no specified a dataStrategy
3957
4274
  async function callLoaderOrAction(
3958
4275
  type: "loader" | "action",
3959
4276
  request: Request,
3960
4277
  match: AgnosticDataRouteMatch,
3961
- matches: AgnosticDataRouteMatch[],
3962
4278
  manifest: RouteManifest,
3963
4279
  mapRouteProperties: MapRoutePropertiesFunction,
3964
- basename: string,
3965
- v7_relativeSplatPath: boolean,
3966
- opts: {
3967
- isStaticRequest?: boolean;
3968
- isRouteRequest?: boolean;
3969
- requestContext?: unknown;
3970
- } = {}
3971
- ): Promise<DataResult> {
3972
- let resultType;
3973
- let result;
4280
+ handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
4281
+ staticContext?: unknown
4282
+ ): Promise<HandlerResult> {
4283
+ let result: HandlerResult;
3974
4284
  let onReject: (() => void) | undefined;
3975
4285
 
3976
- let runHandler = (handler: ActionFunction | LoaderFunction) => {
4286
+ let runHandler = (
4287
+ handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
4288
+ ): Promise<HandlerResult> => {
3977
4289
  // Setup a promise we can race against so that abort signals short circuit
3978
4290
  let reject: () => void;
3979
- let abortPromise = new Promise((_, r) => (reject = r));
4291
+ // This will never resolve so safe to type it as Promise<HandlerResult> to
4292
+ // satisfy the function return value
4293
+ let abortPromise = new Promise<HandlerResult>((_, r) => (reject = r));
3980
4294
  onReject = () => reject();
3981
4295
  request.signal.addEventListener("abort", onReject);
3982
- return Promise.race([
3983
- handler({
3984
- request,
3985
- params: match.params,
3986
- context: opts.requestContext,
3987
- }),
3988
- abortPromise,
3989
- ]);
4296
+
4297
+ let actualHandler = (ctx?: unknown) => {
4298
+ if (typeof handler !== "function") {
4299
+ return Promise.reject(
4300
+ new Error(
4301
+ `You cannot call the handler for a route which defines a boolean ` +
4302
+ `"${type}" [routeId: ${match.route.id}]`
4303
+ )
4304
+ );
4305
+ }
4306
+ return handler(
4307
+ {
4308
+ request,
4309
+ params: match.params,
4310
+ context: staticContext,
4311
+ },
4312
+ ...(ctx !== undefined ? [ctx] : [])
4313
+ );
4314
+ };
4315
+
4316
+ let handlerPromise: Promise<HandlerResult>;
4317
+ if (handlerOverride) {
4318
+ handlerPromise = handlerOverride((ctx: unknown) => actualHandler(ctx));
4319
+ } else {
4320
+ handlerPromise = (async () => {
4321
+ try {
4322
+ let val = await actualHandler();
4323
+ return { type: "data", result: val };
4324
+ } catch (e) {
4325
+ return { type: "error", result: e };
4326
+ }
4327
+ })();
4328
+ }
4329
+
4330
+ return Promise.race([handlerPromise, abortPromise]);
3990
4331
  };
3991
4332
 
3992
4333
  try {
@@ -3996,7 +4337,7 @@ async function callLoaderOrAction(
3996
4337
  if (handler) {
3997
4338
  // Run statically defined handler in parallel with lazy()
3998
4339
  let handlerError;
3999
- let values = await Promise.all([
4340
+ let [value] = await Promise.all([
4000
4341
  // If the handler throws, don't let it immediately bubble out,
4001
4342
  // since we need to let the lazy() execution finish so we know if this
4002
4343
  // route has a boundary that can handle the error
@@ -4005,17 +4346,17 @@ async function callLoaderOrAction(
4005
4346
  }),
4006
4347
  loadLazyRouteModule(match.route, mapRouteProperties, manifest),
4007
4348
  ]);
4008
- if (handlerError) {
4349
+ if (handlerError !== undefined) {
4009
4350
  throw handlerError;
4010
4351
  }
4011
- result = values[0];
4352
+ result = value!;
4012
4353
  } else {
4013
4354
  // Load lazy route module, then run any returned handler
4014
4355
  await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4015
4356
 
4016
4357
  handler = match.route[type];
4017
4358
  if (handler) {
4018
- // Handler still run even if we got interrupted to maintain consistency
4359
+ // Handler still runs even if we got interrupted to maintain consistency
4019
4360
  // with un-abortable behavior of handler execution on non-lazy or
4020
4361
  // previously-lazy-loaded routes
4021
4362
  result = await runHandler(handler);
@@ -4030,7 +4371,7 @@ async function callLoaderOrAction(
4030
4371
  } else {
4031
4372
  // lazy() route has no loader to run. Short circuit here so we don't
4032
4373
  // hit the invariant below that errors on returning undefined.
4033
- return { type: ResultType.data, data: undefined };
4374
+ return { type: ResultType.data, result: undefined };
4034
4375
  }
4035
4376
  }
4036
4377
  } else if (!handler) {
@@ -4044,85 +4385,31 @@ async function callLoaderOrAction(
4044
4385
  }
4045
4386
 
4046
4387
  invariant(
4047
- result !== undefined,
4388
+ result.result !== undefined,
4048
4389
  `You defined ${type === "action" ? "an action" : "a loader"} for route ` +
4049
4390
  `"${match.route.id}" but didn't return anything from your \`${type}\` ` +
4050
4391
  `function. Please return a value or \`null\`.`
4051
4392
  );
4052
4393
  } catch (e) {
4053
- resultType = ResultType.error;
4054
- result = e;
4394
+ // We should already be catching and converting normal handler executions to
4395
+ // HandlerResults and returning them, so anything that throws here is an
4396
+ // unexpected error we still need to wrap
4397
+ return { type: ResultType.error, result: e };
4055
4398
  } finally {
4056
4399
  if (onReject) {
4057
4400
  request.signal.removeEventListener("abort", onReject);
4058
4401
  }
4059
4402
  }
4060
4403
 
4061
- if (isResponse(result)) {
4062
- let status = result.status;
4063
-
4064
- // Process redirects
4065
- if (redirectStatusCodes.has(status)) {
4066
- let location = result.headers.get("Location");
4067
- invariant(
4068
- location,
4069
- "Redirects returned/thrown from loaders/actions must have a Location header"
4070
- );
4071
-
4072
- // Support relative routing in internal redirects
4073
- if (!ABSOLUTE_URL_REGEX.test(location)) {
4074
- location = normalizeTo(
4075
- new URL(request.url),
4076
- matches.slice(0, matches.indexOf(match) + 1),
4077
- basename,
4078
- true,
4079
- location,
4080
- v7_relativeSplatPath
4081
- );
4082
- } else if (!opts.isStaticRequest) {
4083
- // Strip off the protocol+origin for same-origin + same-basename absolute
4084
- // redirects. If this is a static request, we can let it go back to the
4085
- // browser as-is
4086
- let currentUrl = new URL(request.url);
4087
- let url = location.startsWith("//")
4088
- ? new URL(currentUrl.protocol + location)
4089
- : new URL(location);
4090
- let isSameBasename = stripBasename(url.pathname, basename) != null;
4091
- if (url.origin === currentUrl.origin && isSameBasename) {
4092
- location = url.pathname + url.search + url.hash;
4093
- }
4094
- }
4095
-
4096
- // Don't process redirects in the router during static requests requests.
4097
- // Instead, throw the Response and let the server handle it with an HTTP
4098
- // redirect. We also update the Location header in place in this flow so
4099
- // basename and relative routing is taken into account
4100
- if (opts.isStaticRequest) {
4101
- result.headers.set("Location", location);
4102
- throw result;
4103
- }
4104
-
4105
- return {
4106
- type: ResultType.redirect,
4107
- status,
4108
- location,
4109
- revalidate: result.headers.get("X-Remix-Revalidate") !== null,
4110
- reloadDocument: result.headers.get("X-Remix-Reload-Document") !== null,
4111
- };
4112
- }
4404
+ return result;
4405
+ }
4113
4406
 
4114
- // For SSR single-route requests, we want to hand Responses back directly
4115
- // without unwrapping. We do this with the QueryRouteResponse wrapper
4116
- // interface so we can know whether it was returned or thrown
4117
- if (opts.isRouteRequest) {
4118
- let queryRouteResponse: QueryRouteResponse = {
4119
- type:
4120
- resultType === ResultType.error ? ResultType.error : ResultType.data,
4121
- response: result,
4122
- };
4123
- throw queryRouteResponse;
4124
- }
4407
+ async function convertHandlerResultToDataResult(
4408
+ handlerResult: HandlerResult
4409
+ ): Promise<DataResult> {
4410
+ let { result, type, status } = handlerResult;
4125
4411
 
4412
+ if (isResponse(result)) {
4126
4413
  let data: any;
4127
4414
 
4128
4415
  try {
@@ -4142,10 +4429,11 @@ async function callLoaderOrAction(
4142
4429
  return { type: ResultType.error, error: e };
4143
4430
  }
4144
4431
 
4145
- if (resultType === ResultType.error) {
4432
+ if (type === ResultType.error) {
4146
4433
  return {
4147
- type: resultType,
4148
- error: new ErrorResponseImpl(status, result.statusText, data),
4434
+ type: ResultType.error,
4435
+ error: new ErrorResponseImpl(result.status, result.statusText, data),
4436
+ statusCode: result.status,
4149
4437
  headers: result.headers,
4150
4438
  };
4151
4439
  }
@@ -4158,8 +4446,12 @@ async function callLoaderOrAction(
4158
4446
  };
4159
4447
  }
4160
4448
 
4161
- if (resultType === ResultType.error) {
4162
- return { type: resultType, error: result };
4449
+ if (type === ResultType.error) {
4450
+ return {
4451
+ type: ResultType.error,
4452
+ error: result,
4453
+ statusCode: isRouteErrorResponse(result) ? result.status : status,
4454
+ };
4163
4455
  }
4164
4456
 
4165
4457
  if (isDeferredData(result)) {
@@ -4171,7 +4463,60 @@ async function callLoaderOrAction(
4171
4463
  };
4172
4464
  }
4173
4465
 
4174
- return { type: ResultType.data, data: result };
4466
+ return { type: ResultType.data, data: result, statusCode: status };
4467
+ }
4468
+
4469
+ // Support relative routing in internal redirects
4470
+ function normalizeRelativeRoutingRedirectResponse(
4471
+ response: Response,
4472
+ request: Request,
4473
+ routeId: string,
4474
+ matches: AgnosticDataRouteMatch[],
4475
+ basename: string,
4476
+ v7_relativeSplatPath: boolean
4477
+ ) {
4478
+ let location = response.headers.get("Location");
4479
+ invariant(
4480
+ location,
4481
+ "Redirects returned/thrown from loaders/actions must have a Location header"
4482
+ );
4483
+
4484
+ if (!ABSOLUTE_URL_REGEX.test(location)) {
4485
+ let trimmedMatches = matches.slice(
4486
+ 0,
4487
+ matches.findIndex((m) => m.route.id === routeId) + 1
4488
+ );
4489
+ location = normalizeTo(
4490
+ new URL(request.url),
4491
+ trimmedMatches,
4492
+ basename,
4493
+ true,
4494
+ location,
4495
+ v7_relativeSplatPath
4496
+ );
4497
+ response.headers.set("Location", location);
4498
+ }
4499
+
4500
+ return response;
4501
+ }
4502
+
4503
+ function normalizeRedirectLocation(
4504
+ location: string,
4505
+ currentUrl: URL,
4506
+ basename: string
4507
+ ): string {
4508
+ if (ABSOLUTE_URL_REGEX.test(location)) {
4509
+ // Strip off the protocol+origin for same-origin + same-basename absolute redirects
4510
+ let normalizedLocation = location;
4511
+ let url = normalizedLocation.startsWith("//")
4512
+ ? new URL(currentUrl.protocol + normalizedLocation)
4513
+ : new URL(normalizedLocation);
4514
+ let isSameBasename = stripBasename(url.pathname, basename) != null;
4515
+ if (url.origin === currentUrl.origin && isSameBasename) {
4516
+ return url.pathname + url.search + url.hash;
4517
+ }
4518
+ }
4519
+ return location;
4175
4520
  }
4176
4521
 
4177
4522
  // Utility method for creating the Request instances for loaders/actions during
@@ -4239,8 +4584,9 @@ function processRouteLoaderData(
4239
4584
  matches: AgnosticDataRouteMatch[],
4240
4585
  matchesToLoad: AgnosticDataRouteMatch[],
4241
4586
  results: DataResult[],
4242
- pendingError: RouteData | undefined,
4243
- activeDeferreds: Map<string, DeferredData>
4587
+ pendingActionResult: PendingActionResult | undefined,
4588
+ activeDeferreds: Map<string, DeferredData>,
4589
+ skipLoaderErrorBubbling: boolean
4244
4590
  ): {
4245
4591
  loaderData: RouterState["loaderData"];
4246
4592
  errors: RouterState["errors"] | null;
@@ -4253,6 +4599,10 @@ function processRouteLoaderData(
4253
4599
  let statusCode: number | undefined;
4254
4600
  let foundError = false;
4255
4601
  let loaderHeaders: Record<string, Headers> = {};
4602
+ let pendingError =
4603
+ pendingActionResult && isErrorResult(pendingActionResult[1])
4604
+ ? pendingActionResult[1].error
4605
+ : undefined;
4256
4606
 
4257
4607
  // Process loader results into state.loaderData/state.errors
4258
4608
  results.forEach((result, index) => {
@@ -4262,23 +4612,27 @@ function processRouteLoaderData(
4262
4612
  "Cannot handle redirect results in processLoaderData"
4263
4613
  );
4264
4614
  if (isErrorResult(result)) {
4265
- // Look upwards from the matched route for the closest ancestor
4266
- // error boundary, defaulting to the root match
4267
- let boundaryMatch = findNearestBoundary(matches, id);
4268
4615
  let error = result.error;
4269
4616
  // If we have a pending action error, we report it at the highest-route
4270
4617
  // that throws a loader error, and then clear it out to indicate that
4271
4618
  // it was consumed
4272
- if (pendingError) {
4273
- error = Object.values(pendingError)[0];
4619
+ if (pendingError !== undefined) {
4620
+ error = pendingError;
4274
4621
  pendingError = undefined;
4275
4622
  }
4276
4623
 
4277
4624
  errors = errors || {};
4278
4625
 
4279
- // Prefer higher error values if lower errors bubble to the same boundary
4280
- if (errors[boundaryMatch.route.id] == null) {
4281
- errors[boundaryMatch.route.id] = error;
4626
+ if (skipLoaderErrorBubbling) {
4627
+ errors[id] = error;
4628
+ } else {
4629
+ // Look upwards from the matched route for the closest ancestor error
4630
+ // boundary, defaulting to the root match. Prefer higher error values
4631
+ // if lower errors bubble to the same boundary
4632
+ let boundaryMatch = findNearestBoundary(matches, id);
4633
+ if (errors[boundaryMatch.route.id] == null) {
4634
+ errors[boundaryMatch.route.id] = error;
4635
+ }
4282
4636
  }
4283
4637
 
4284
4638
  // Clear our any prior loaderData for the throwing route
@@ -4299,21 +4653,28 @@ function processRouteLoaderData(
4299
4653
  if (isDeferredResult(result)) {
4300
4654
  activeDeferreds.set(id, result.deferredData);
4301
4655
  loaderData[id] = result.deferredData.data;
4656
+ // Error status codes always override success status codes, but if all
4657
+ // loaders are successful we take the deepest status code.
4658
+ if (
4659
+ result.statusCode != null &&
4660
+ result.statusCode !== 200 &&
4661
+ !foundError
4662
+ ) {
4663
+ statusCode = result.statusCode;
4664
+ }
4665
+ if (result.headers) {
4666
+ loaderHeaders[id] = result.headers;
4667
+ }
4302
4668
  } else {
4303
4669
  loaderData[id] = result.data;
4304
- }
4305
-
4306
- // Error status codes always override success status codes, but if all
4307
- // loaders are successful we take the deepest status code.
4308
- if (
4309
- result.statusCode != null &&
4310
- result.statusCode !== 200 &&
4311
- !foundError
4312
- ) {
4313
- statusCode = result.statusCode;
4314
- }
4315
- if (result.headers) {
4316
- loaderHeaders[id] = result.headers;
4670
+ // Error status codes always override success status codes, but if all
4671
+ // loaders are successful we take the deepest status code.
4672
+ if (result.statusCode && result.statusCode !== 200 && !foundError) {
4673
+ statusCode = result.statusCode;
4674
+ }
4675
+ if (result.headers) {
4676
+ loaderHeaders[id] = result.headers;
4677
+ }
4317
4678
  }
4318
4679
  }
4319
4680
  });
@@ -4321,9 +4682,9 @@ function processRouteLoaderData(
4321
4682
  // If we didn't consume the pending action error (i.e., all loaders
4322
4683
  // resolved), then consume it here. Also clear out any loaderData for the
4323
4684
  // throwing route
4324
- if (pendingError) {
4325
- errors = pendingError;
4326
- loaderData[Object.keys(pendingError)[0]] = undefined;
4685
+ if (pendingError !== undefined && pendingActionResult) {
4686
+ errors = { [pendingActionResult[0]]: pendingError };
4687
+ loaderData[pendingActionResult[0]] = undefined;
4327
4688
  }
4328
4689
 
4329
4690
  return {
@@ -4339,7 +4700,7 @@ function processLoaderData(
4339
4700
  matches: AgnosticDataRouteMatch[],
4340
4701
  matchesToLoad: AgnosticDataRouteMatch[],
4341
4702
  results: DataResult[],
4342
- pendingError: RouteData | undefined,
4703
+ pendingActionResult: PendingActionResult | undefined,
4343
4704
  revalidatingFetchers: RevalidatingFetcher[],
4344
4705
  fetcherResults: DataResult[],
4345
4706
  activeDeferreds: Map<string, DeferredData>
@@ -4351,8 +4712,9 @@ function processLoaderData(
4351
4712
  matches,
4352
4713
  matchesToLoad,
4353
4714
  results,
4354
- pendingError,
4355
- activeDeferreds
4715
+ pendingActionResult,
4716
+ activeDeferreds,
4717
+ false // This method is only called client side so we always want to bubble
4356
4718
  );
4357
4719
 
4358
4720
  // Process results from our revalidating fetchers
@@ -4425,6 +4787,24 @@ function mergeLoaderData(
4425
4787
  return mergedLoaderData;
4426
4788
  }
4427
4789
 
4790
+ function getActionDataForCommit(
4791
+ pendingActionResult: PendingActionResult | undefined
4792
+ ) {
4793
+ if (!pendingActionResult) {
4794
+ return {};
4795
+ }
4796
+ return isErrorResult(pendingActionResult[1])
4797
+ ? {
4798
+ // Clear out prior actionData on errors
4799
+ actionData: {},
4800
+ }
4801
+ : {
4802
+ actionData: {
4803
+ [pendingActionResult[0]]: pendingActionResult[1].data,
4804
+ },
4805
+ };
4806
+ }
4807
+
4428
4808
  // Find the nearest error boundary, looking upwards from the leaf route (or the
4429
4809
  // route specified by routeId) for the closest ancestor error boundary,
4430
4810
  // defaulting to the root match
@@ -4559,6 +4939,22 @@ function isHashChangeOnly(a: Location, b: Location): boolean {
4559
4939
  return false;
4560
4940
  }
4561
4941
 
4942
+ function isHandlerResult(result: unknown): result is HandlerResult {
4943
+ return (
4944
+ result != null &&
4945
+ typeof result === "object" &&
4946
+ "type" in result &&
4947
+ "result" in result &&
4948
+ (result.type === ResultType.data || result.type === ResultType.error)
4949
+ );
4950
+ }
4951
+
4952
+ function isRedirectHandlerResult(result: HandlerResult) {
4953
+ return (
4954
+ isResponse(result.result) && redirectStatusCodes.has(result.result.status)
4955
+ );
4956
+ }
4957
+
4562
4958
  function isDeferredResult(result: DataResult): result is DeferredResult {
4563
4959
  return result.type === ResultType.deferred;
4564
4960
  }
@@ -4603,14 +4999,6 @@ function isRedirectResponse(result: any): result is Response {
4603
4999
  return status >= 300 && status <= 399 && location != null;
4604
5000
  }
4605
5001
 
4606
- function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
4607
- return (
4608
- obj &&
4609
- isResponse(obj.response) &&
4610
- (obj.type === ResultType.data || obj.type === ResultType.error)
4611
- );
4612
- }
4613
-
4614
5002
  function isValidMethod(method: string): method is FormMethod | V7_FormMethod {
4615
5003
  return validRequestMethods.has(method.toLowerCase() as FormMethod);
4616
5004
  }