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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/router.ts CHANGED
@@ -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,11 @@ export interface StaticHandler {
400
404
  dataRoutes: AgnosticDataRouteObject[];
401
405
  query(
402
406
  request: Request,
403
- opts?: { requestContext?: unknown }
407
+ opts?: {
408
+ requestContext?: unknown;
409
+ skipLoaderErrorBubbling?: boolean;
410
+ unstable_dataStrategy?: DataStrategyFunction;
411
+ }
404
412
  ): Promise<StaticHandlerContext | Response>;
405
413
  queryRoute(
406
414
  request: Request,
@@ -616,18 +624,14 @@ interface ShortCircuitable {
616
624
  shortCircuited?: boolean;
617
625
  }
618
626
 
627
+ type PendingActionResult = [string, SuccessResult | ErrorResult];
628
+
619
629
  interface HandleActionResult extends ShortCircuitable {
620
630
  /**
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
631
+ * Tuple for the returned or thrown value from the current action. The routeId
632
+ * is the action route for success and the bubbled boundary route for errors.
629
633
  */
630
- pendingActionData?: RouteData;
634
+ pendingActionResult?: PendingActionResult;
631
635
  }
632
636
 
633
637
  interface HandleLoadersResult extends ShortCircuitable {
@@ -660,16 +664,6 @@ interface RevalidatingFetcher extends FetchLoadMatch {
660
664
  controller: AbortController | null;
661
665
  }
662
666
 
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
667
  const validMutationMethodsArr: MutationFormMethod[] = [
674
668
  "post",
675
669
  "put",
@@ -776,6 +770,7 @@ export function createRouter(init: RouterInit): Router {
776
770
  );
777
771
  let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
778
772
  let basename = init.basename || "/";
773
+ let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
779
774
  // Config driven behavior flags
780
775
  let future: FutureConfig = {
781
776
  v7_fetcherPersist: false,
@@ -783,6 +778,7 @@ export function createRouter(init: RouterInit): Router {
783
778
  v7_partialHydration: false,
784
779
  v7_prependBasename: false,
785
780
  v7_relativeSplatPath: false,
781
+ unstable_skipActionErrorRevalidation: false,
786
782
  ...init.future,
787
783
  };
788
784
  // Cleanup function for history
@@ -835,9 +831,16 @@ export function createRouter(init: RouterInit): Router {
835
831
  let errors = init.hydrationData ? init.hydrationData.errors : null;
836
832
  let isRouteInitialized = (m: AgnosticDataRouteMatch) => {
837
833
  // No loader, nothing to initialize
838
- if (!m.route.loader) return true;
834
+ if (!m.route.loader) {
835
+ return true;
836
+ }
839
837
  // Explicitly opting-in to running on hydration
840
- if (m.route.loader.hydrate === true) return false;
838
+ if (
839
+ typeof m.route.loader === "function" &&
840
+ m.route.loader.hydrate === true
841
+ ) {
842
+ return false;
843
+ }
841
844
  // Otherwise, initialized if hydrated with data or an error
842
845
  return (
843
846
  (loaderData && loaderData[m.route.id] !== undefined) ||
@@ -1493,24 +1496,24 @@ export function createRouter(init: RouterInit): Router {
1493
1496
  pendingNavigationController.signal,
1494
1497
  opts && opts.submission
1495
1498
  );
1496
- let pendingActionData: RouteData | undefined;
1497
- let pendingError: RouteData | undefined;
1499
+ let pendingActionResult: PendingActionResult | undefined;
1498
1500
 
1499
1501
  if (opts && opts.pendingError) {
1500
1502
  // If we have a pendingError, it means the user attempted a GET submission
1501
1503
  // with binary FormData so assign here and skip to handleLoaders. That
1502
1504
  // way we handle calling loaders above the boundary etc. It's not really
1503
1505
  // different from an actionError in that sense.
1504
- pendingError = {
1505
- [findNearestBoundary(matches).route.id]: opts.pendingError,
1506
- };
1506
+ pendingActionResult = [
1507
+ findNearestBoundary(matches).route.id,
1508
+ { type: ResultType.error, error: opts.pendingError },
1509
+ ];
1507
1510
  } else if (
1508
1511
  opts &&
1509
1512
  opts.submission &&
1510
1513
  isMutationMethod(opts.submission.formMethod)
1511
1514
  ) {
1512
1515
  // Call action if we received an action submission
1513
- let actionOutput = await handleAction(
1516
+ let actionResult = await handleAction(
1514
1517
  request,
1515
1518
  location,
1516
1519
  opts.submission,
@@ -1518,17 +1521,20 @@ export function createRouter(init: RouterInit): Router {
1518
1521
  { replace: opts.replace, flushSync }
1519
1522
  );
1520
1523
 
1521
- if (actionOutput.shortCircuited) {
1524
+ if (actionResult.shortCircuited) {
1522
1525
  return;
1523
1526
  }
1524
1527
 
1525
- pendingActionData = actionOutput.pendingActionData;
1526
- pendingError = actionOutput.pendingActionError;
1528
+ pendingActionResult = actionResult.pendingActionResult;
1527
1529
  loadingNavigation = getLoadingNavigation(location, opts.submission);
1528
1530
  flushSync = false;
1529
1531
 
1530
1532
  // Create a GET request for the loaders
1531
- request = new Request(request.url, { signal: request.signal });
1533
+ request = createClientSideRequest(
1534
+ init.history,
1535
+ request.url,
1536
+ request.signal
1537
+ );
1532
1538
  }
1533
1539
 
1534
1540
  // Call loaders
@@ -1542,8 +1548,7 @@ export function createRouter(init: RouterInit): Router {
1542
1548
  opts && opts.replace,
1543
1549
  opts && opts.initialHydration === true,
1544
1550
  flushSync,
1545
- pendingActionData,
1546
- pendingError
1551
+ pendingActionResult
1547
1552
  );
1548
1553
 
1549
1554
  if (shortCircuited) {
@@ -1557,7 +1562,7 @@ export function createRouter(init: RouterInit): Router {
1557
1562
 
1558
1563
  completeNavigation(location, {
1559
1564
  matches,
1560
- ...(pendingActionData ? { actionData: pendingActionData } : {}),
1565
+ ...getActionDataForCommit(pendingActionResult),
1561
1566
  loaderData,
1562
1567
  errors,
1563
1568
  });
@@ -1592,16 +1597,13 @@ export function createRouter(init: RouterInit): Router {
1592
1597
  }),
1593
1598
  };
1594
1599
  } else {
1595
- result = await callLoaderOrAction(
1600
+ let results = await callDataStrategy(
1596
1601
  "action",
1597
1602
  request,
1598
- actionMatch,
1599
- matches,
1600
- manifest,
1601
- mapRouteProperties,
1602
- basename,
1603
- future.v7_relativeSplatPath
1603
+ [actionMatch],
1604
+ matches
1604
1605
  );
1606
+ result = results[0];
1605
1607
 
1606
1608
  if (request.signal.aborted) {
1607
1609
  return { shortCircuited: true };
@@ -1616,13 +1618,24 @@ export function createRouter(init: RouterInit): Router {
1616
1618
  // If the user didn't explicity indicate replace behavior, replace if
1617
1619
  // we redirected to the exact same location we're currently at to avoid
1618
1620
  // double back-buttons
1619
- replace =
1620
- result.location === state.location.pathname + state.location.search;
1621
+ let location = normalizeRedirectLocation(
1622
+ result.response.headers.get("Location")!,
1623
+ new URL(request.url),
1624
+ basename
1625
+ );
1626
+ replace = location === state.location.pathname + state.location.search;
1621
1627
  }
1622
- await startRedirectNavigation(state, result, { submission, replace });
1628
+ await startRedirectNavigation(request, result, {
1629
+ submission,
1630
+ replace,
1631
+ });
1623
1632
  return { shortCircuited: true };
1624
1633
  }
1625
1634
 
1635
+ if (isDeferredResult(result)) {
1636
+ throw getInternalRouterError(400, { type: "defer-action" });
1637
+ }
1638
+
1626
1639
  if (isErrorResult(result)) {
1627
1640
  // Store off the pending error - we use it to determine which loaders
1628
1641
  // to call and will commit it when we complete the navigation
@@ -1637,18 +1650,12 @@ export function createRouter(init: RouterInit): Router {
1637
1650
  }
1638
1651
 
1639
1652
  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 },
1653
+ pendingActionResult: [boundaryMatch.route.id, result],
1643
1654
  };
1644
1655
  }
1645
1656
 
1646
- if (isDeferredResult(result)) {
1647
- throw getInternalRouterError(400, { type: "defer-action" });
1648
- }
1649
-
1650
1657
  return {
1651
- pendingActionData: { [actionMatch.route.id]: result.data },
1658
+ pendingActionResult: [actionMatch.route.id, result],
1652
1659
  };
1653
1660
  }
1654
1661
 
@@ -1664,8 +1671,7 @@ export function createRouter(init: RouterInit): Router {
1664
1671
  replace?: boolean,
1665
1672
  initialHydration?: boolean,
1666
1673
  flushSync?: boolean,
1667
- pendingActionData?: RouteData,
1668
- pendingError?: RouteData
1674
+ pendingActionResult?: PendingActionResult
1669
1675
  ): Promise<HandleLoadersResult> {
1670
1676
  // Figure out the right navigation we want to use for data loading
1671
1677
  let loadingNavigation =
@@ -1686,6 +1692,7 @@ export function createRouter(init: RouterInit): Router {
1686
1692
  activeSubmission,
1687
1693
  location,
1688
1694
  future.v7_partialHydration && initialHydration === true,
1695
+ future.unstable_skipActionErrorRevalidation,
1689
1696
  isRevalidationRequired,
1690
1697
  cancelledDeferredRoutes,
1691
1698
  cancelledFetcherLoads,
@@ -1694,8 +1701,7 @@ export function createRouter(init: RouterInit): Router {
1694
1701
  fetchRedirectIds,
1695
1702
  routesToUse,
1696
1703
  basename,
1697
- pendingActionData,
1698
- pendingError
1704
+ pendingActionResult
1699
1705
  );
1700
1706
 
1701
1707
  // Cancel pending deferreds for no-longer-matched routes or routes we're
@@ -1718,8 +1724,11 @@ export function createRouter(init: RouterInit): Router {
1718
1724
  matches,
1719
1725
  loaderData: {},
1720
1726
  // Commit pending error if we're short circuiting
1721
- errors: pendingError || null,
1722
- ...(pendingActionData ? { actionData: pendingActionData } : {}),
1727
+ errors:
1728
+ pendingActionResult && isErrorResult(pendingActionResult[1])
1729
+ ? { [pendingActionResult[0]]: pendingActionResult[1].error }
1730
+ : null,
1731
+ ...getActionDataForCommit(pendingActionResult),
1723
1732
  ...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),
1724
1733
  },
1725
1734
  { flushSync }
@@ -1745,15 +1754,27 @@ export function createRouter(init: RouterInit): Router {
1745
1754
  );
1746
1755
  state.fetchers.set(rf.key, revalidatingFetcher);
1747
1756
  });
1748
- let actionData = pendingActionData || state.actionData;
1757
+
1758
+ let actionData: Record<string, RouteData> | null | undefined;
1759
+ if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
1760
+ // This is cast to `any` currently because `RouteData`uses any and it
1761
+ // would be a breaking change to use any.
1762
+ // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
1763
+ actionData = {
1764
+ [pendingActionResult[0]]: pendingActionResult[1].data as any,
1765
+ };
1766
+ } else if (state.actionData) {
1767
+ if (Object.keys(state.actionData).length === 0) {
1768
+ actionData = null;
1769
+ } else {
1770
+ actionData = state.actionData;
1771
+ }
1772
+ }
1773
+
1749
1774
  updateState(
1750
1775
  {
1751
1776
  navigation: loadingNavigation,
1752
- ...(actionData
1753
- ? Object.keys(actionData).length === 0
1754
- ? { actionData: null }
1755
- : { actionData }
1756
- : {}),
1777
+ ...(actionData !== undefined ? { actionData } : {}),
1757
1778
  ...(revalidatingFetchers.length > 0
1758
1779
  ? { fetchers: new Map(state.fetchers) }
1759
1780
  : {}),
@@ -1786,7 +1807,7 @@ export function createRouter(init: RouterInit): Router {
1786
1807
  );
1787
1808
  }
1788
1809
 
1789
- let { results, loaderResults, fetcherResults } =
1810
+ let { loaderResults, fetcherResults } =
1790
1811
  await callLoadersAndMaybeResolveData(
1791
1812
  state.matches,
1792
1813
  matches,
@@ -1811,7 +1832,7 @@ export function createRouter(init: RouterInit): Router {
1811
1832
  revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
1812
1833
 
1813
1834
  // If any loaders returned a redirect Response, start a new REPLACE navigation
1814
- let redirect = findRedirect(results);
1835
+ let redirect = findRedirect([...loaderResults, ...fetcherResults]);
1815
1836
  if (redirect) {
1816
1837
  if (redirect.idx >= matchesToLoad.length) {
1817
1838
  // If this redirect came from a fetcher make sure we mark it in
@@ -1821,7 +1842,9 @@ export function createRouter(init: RouterInit): Router {
1821
1842
  revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
1822
1843
  fetchRedirectIds.add(fetcherKey);
1823
1844
  }
1824
- await startRedirectNavigation(state, redirect.result, { replace });
1845
+ await startRedirectNavigation(request, redirect.result, {
1846
+ replace,
1847
+ });
1825
1848
  return { shortCircuited: true };
1826
1849
  }
1827
1850
 
@@ -1831,7 +1854,7 @@ export function createRouter(init: RouterInit): Router {
1831
1854
  matches,
1832
1855
  matchesToLoad,
1833
1856
  loaderResults,
1834
- pendingError,
1857
+ pendingActionResult,
1835
1858
  revalidatingFetchers,
1836
1859
  fetcherResults,
1837
1860
  activeDeferreds
@@ -1995,16 +2018,13 @@ export function createRouter(init: RouterInit): Router {
1995
2018
  fetchControllers.set(key, abortController);
1996
2019
 
1997
2020
  let originatingLoadId = incrementingLoadId;
1998
- let actionResult = await callLoaderOrAction(
2021
+ let actionResults = await callDataStrategy(
1999
2022
  "action",
2000
2023
  fetchRequest,
2001
- match,
2002
- requestMatches,
2003
- manifest,
2004
- mapRouteProperties,
2005
- basename,
2006
- future.v7_relativeSplatPath
2024
+ [match],
2025
+ requestMatches
2007
2026
  );
2027
+ let actionResult = actionResults[0];
2008
2028
 
2009
2029
  if (fetchRequest.signal.aborted) {
2010
2030
  // We can delete this so long as we weren't aborted by our own fetcher
@@ -2037,7 +2057,7 @@ export function createRouter(init: RouterInit): Router {
2037
2057
  } else {
2038
2058
  fetchRedirectIds.add(key);
2039
2059
  updateFetcherState(key, getLoadingFetcher(submission));
2040
- return startRedirectNavigation(state, actionResult, {
2060
+ return startRedirectNavigation(fetchRequest, actionResult, {
2041
2061
  fetcherSubmission: submission,
2042
2062
  });
2043
2063
  }
@@ -2083,6 +2103,7 @@ export function createRouter(init: RouterInit): Router {
2083
2103
  submission,
2084
2104
  nextLocation,
2085
2105
  false,
2106
+ future.unstable_skipActionErrorRevalidation,
2086
2107
  isRevalidationRequired,
2087
2108
  cancelledDeferredRoutes,
2088
2109
  cancelledFetcherLoads,
@@ -2091,8 +2112,7 @@ export function createRouter(init: RouterInit): Router {
2091
2112
  fetchRedirectIds,
2092
2113
  routesToUse,
2093
2114
  basename,
2094
- { [match.route.id]: actionResult.data },
2095
- undefined // No need to send through errors since we short circuit above
2115
+ [match.route.id, actionResult]
2096
2116
  );
2097
2117
 
2098
2118
  // Put all revalidating fetchers into the loading state, except for the
@@ -2126,7 +2146,7 @@ export function createRouter(init: RouterInit): Router {
2126
2146
  abortPendingFetchRevalidations
2127
2147
  );
2128
2148
 
2129
- let { results, loaderResults, fetcherResults } =
2149
+ let { loaderResults, fetcherResults } =
2130
2150
  await callLoadersAndMaybeResolveData(
2131
2151
  state.matches,
2132
2152
  matches,
@@ -2148,7 +2168,7 @@ export function createRouter(init: RouterInit): Router {
2148
2168
  fetchControllers.delete(key);
2149
2169
  revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
2150
2170
 
2151
- let redirect = findRedirect(results);
2171
+ let redirect = findRedirect([...loaderResults, ...fetcherResults]);
2152
2172
  if (redirect) {
2153
2173
  if (redirect.idx >= matchesToLoad.length) {
2154
2174
  // If this redirect came from a fetcher make sure we mark it in
@@ -2158,7 +2178,7 @@ export function createRouter(init: RouterInit): Router {
2158
2178
  revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2159
2179
  fetchRedirectIds.add(fetcherKey);
2160
2180
  }
2161
- return startRedirectNavigation(state, redirect.result);
2181
+ return startRedirectNavigation(revalidationRequest, redirect.result);
2162
2182
  }
2163
2183
 
2164
2184
  // Process and commit output from loaders
@@ -2246,16 +2266,13 @@ export function createRouter(init: RouterInit): Router {
2246
2266
  fetchControllers.set(key, abortController);
2247
2267
 
2248
2268
  let originatingLoadId = incrementingLoadId;
2249
- let result: DataResult = await callLoaderOrAction(
2269
+ let results = await callDataStrategy(
2250
2270
  "loader",
2251
2271
  fetchRequest,
2252
- match,
2253
- matches,
2254
- manifest,
2255
- mapRouteProperties,
2256
- basename,
2257
- future.v7_relativeSplatPath
2272
+ [match],
2273
+ matches
2258
2274
  );
2275
+ let result = results[0];
2259
2276
 
2260
2277
  // Deferred isn't supported for fetcher loads, await everything and treat it
2261
2278
  // as a normal load. resolveDeferredData will return undefined if this
@@ -2293,7 +2310,7 @@ export function createRouter(init: RouterInit): Router {
2293
2310
  return;
2294
2311
  } else {
2295
2312
  fetchRedirectIds.add(key);
2296
- await startRedirectNavigation(state, result);
2313
+ await startRedirectNavigation(fetchRequest, result);
2297
2314
  return;
2298
2315
  }
2299
2316
  }
@@ -2330,7 +2347,7 @@ export function createRouter(init: RouterInit): Router {
2330
2347
  * the history action from the original navigation (PUSH or REPLACE).
2331
2348
  */
2332
2349
  async function startRedirectNavigation(
2333
- state: RouterState,
2350
+ request: Request,
2334
2351
  redirect: RedirectResult,
2335
2352
  {
2336
2353
  submission,
@@ -2342,26 +2359,29 @@ export function createRouter(init: RouterInit): Router {
2342
2359
  replace?: boolean;
2343
2360
  } = {}
2344
2361
  ) {
2345
- if (redirect.revalidate) {
2362
+ if (redirect.response.headers.has("X-Remix-Revalidate")) {
2346
2363
  isRevalidationRequired = true;
2347
2364
  }
2348
2365
 
2349
- let redirectLocation = createLocation(state.location, redirect.location, {
2366
+ let location = redirect.response.headers.get("Location");
2367
+ invariant(location, "Expected a Location header on the redirect Response");
2368
+ location = normalizeRedirectLocation(
2369
+ location,
2370
+ new URL(request.url),
2371
+ basename
2372
+ );
2373
+ let redirectLocation = createLocation(state.location, location, {
2350
2374
  _isRedirect: true,
2351
2375
  });
2352
- invariant(
2353
- redirectLocation,
2354
- "Expected a location on the redirect navigation"
2355
- );
2356
2376
 
2357
2377
  if (isBrowser) {
2358
2378
  let isDocumentReload = false;
2359
2379
 
2360
- if (redirect.reloadDocument) {
2380
+ if (redirect.response.headers.has("X-Remix-Reload-Document")) {
2361
2381
  // Hard reload if the response contained X-Remix-Reload-Document
2362
2382
  isDocumentReload = true;
2363
- } else if (ABSOLUTE_URL_REGEX.test(redirect.location)) {
2364
- const url = init.history.createURL(redirect.location);
2383
+ } else if (ABSOLUTE_URL_REGEX.test(location)) {
2384
+ const url = init.history.createURL(location);
2365
2385
  isDocumentReload =
2366
2386
  // Hard reload if it's an absolute URL to a new origin
2367
2387
  url.origin !== routerWindow.location.origin ||
@@ -2371,9 +2391,9 @@ export function createRouter(init: RouterInit): Router {
2371
2391
 
2372
2392
  if (isDocumentReload) {
2373
2393
  if (replace) {
2374
- routerWindow.location.replace(redirect.location);
2394
+ routerWindow.location.replace(location);
2375
2395
  } else {
2376
- routerWindow.location.assign(redirect.location);
2396
+ routerWindow.location.assign(location);
2377
2397
  }
2378
2398
  return;
2379
2399
  }
@@ -2404,14 +2424,14 @@ export function createRouter(init: RouterInit): Router {
2404
2424
  // redirected location
2405
2425
  let activeSubmission = submission || fetcherSubmission;
2406
2426
  if (
2407
- redirectPreserveMethodStatusCodes.has(redirect.status) &&
2427
+ redirectPreserveMethodStatusCodes.has(redirect.response.status) &&
2408
2428
  activeSubmission &&
2409
2429
  isMutationMethod(activeSubmission.formMethod)
2410
2430
  ) {
2411
2431
  await startNavigation(redirectHistoryAction, redirectLocation, {
2412
2432
  submission: {
2413
2433
  ...activeSubmission,
2414
- formAction: redirect.location,
2434
+ formAction: location,
2415
2435
  },
2416
2436
  // Preserve this flag across redirects
2417
2437
  preventScrollReset: pendingPreventScrollReset,
@@ -2433,6 +2453,55 @@ export function createRouter(init: RouterInit): Router {
2433
2453
  }
2434
2454
  }
2435
2455
 
2456
+ // Utility wrapper for calling dataStrategy client-side without having to
2457
+ // pass around the manifest, mapRouteProperties, etc.
2458
+ async function callDataStrategy(
2459
+ type: "loader" | "action",
2460
+ request: Request,
2461
+ matchesToLoad: AgnosticDataRouteMatch[],
2462
+ matches: AgnosticDataRouteMatch[]
2463
+ ): Promise<DataResult[]> {
2464
+ try {
2465
+ let results = await callDataStrategyImpl(
2466
+ dataStrategyImpl,
2467
+ type,
2468
+ request,
2469
+ matchesToLoad,
2470
+ matches,
2471
+ manifest,
2472
+ mapRouteProperties
2473
+ );
2474
+
2475
+ return await Promise.all(
2476
+ results.map((result, i) => {
2477
+ if (isRedirectHandlerResult(result)) {
2478
+ let response = result.result as Response;
2479
+ return {
2480
+ type: ResultType.redirect,
2481
+ response: normalizeRelativeRoutingRedirectResponse(
2482
+ response,
2483
+ request,
2484
+ matchesToLoad[i].route.id,
2485
+ matches,
2486
+ basename,
2487
+ future.v7_relativeSplatPath
2488
+ ),
2489
+ };
2490
+ }
2491
+
2492
+ return convertHandlerResultToDataResult(result);
2493
+ })
2494
+ );
2495
+ } catch (e) {
2496
+ // If the outer dataStrategy method throws, just return the error for all
2497
+ // matches - and it'll naturally bubble to the root
2498
+ return matchesToLoad.map(() => ({
2499
+ type: ResultType.error,
2500
+ error: e,
2501
+ }));
2502
+ }
2503
+ }
2504
+
2436
2505
  async function callLoadersAndMaybeResolveData(
2437
2506
  currentMatches: AgnosticDataRouteMatch[],
2438
2507
  matches: AgnosticDataRouteMatch[],
@@ -2440,45 +2509,33 @@ export function createRouter(init: RouterInit): Router {
2440
2509
  fetchersToLoad: RevalidatingFetcher[],
2441
2510
  request: Request
2442
2511
  ) {
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
- ),
2512
+ let [loaderResults, ...fetcherResults] = await Promise.all([
2513
+ matchesToLoad.length
2514
+ ? callDataStrategy("loader", request, matchesToLoad, matches)
2515
+ : [],
2459
2516
  ...fetchersToLoad.map((f) => {
2460
2517
  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
2518
+ let fetcherRequest = createClientSideRequest(
2519
+ init.history,
2520
+ f.path,
2521
+ f.controller.signal
2470
2522
  );
2523
+ return callDataStrategy(
2524
+ "loader",
2525
+ fetcherRequest,
2526
+ [f.match],
2527
+ f.matches
2528
+ ).then((r) => r[0]);
2471
2529
  } else {
2472
- let error: ErrorResult = {
2530
+ return Promise.resolve<DataResult>({
2473
2531
  type: ResultType.error,
2474
- error: getInternalRouterError(404, { pathname: f.path }),
2475
- };
2476
- return error;
2532
+ error: getInternalRouterError(404, {
2533
+ pathname: f.path,
2534
+ }),
2535
+ });
2477
2536
  }
2478
2537
  }),
2479
2538
  ]);
2480
- let loaderResults = results.slice(0, matchesToLoad.length);
2481
- let fetcherResults = results.slice(matchesToLoad.length);
2482
2539
 
2483
2540
  await Promise.all([
2484
2541
  resolveDeferredResults(
@@ -2498,7 +2555,10 @@ export function createRouter(init: RouterInit): Router {
2498
2555
  ),
2499
2556
  ]);
2500
2557
 
2501
- return { results, loaderResults, fetcherResults };
2558
+ return {
2559
+ loaderResults,
2560
+ fetcherResults,
2561
+ };
2502
2562
  }
2503
2563
 
2504
2564
  function interruptActiveLoads() {
@@ -2925,10 +2985,25 @@ export function createStaticHandler(
2925
2985
  * redirect response is returned or thrown from any action/loader. We
2926
2986
  * propagate that out and return the raw Response so the HTTP server can
2927
2987
  * return it directly.
2988
+ *
2989
+ * - `opts.requestContext` is an optional server context that will be passed
2990
+ * to actions/loaders in the `context` parameter
2991
+ * - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
2992
+ * the bubbling of errors which allows single-fetch-type implementations
2993
+ * where the client will handle the bubbling and we may need to return data
2994
+ * for the handling route
2928
2995
  */
2929
2996
  async function query(
2930
2997
  request: Request,
2931
- { requestContext }: { requestContext?: unknown } = {}
2998
+ {
2999
+ requestContext,
3000
+ skipLoaderErrorBubbling,
3001
+ unstable_dataStrategy,
3002
+ }: {
3003
+ requestContext?: unknown;
3004
+ skipLoaderErrorBubbling?: boolean;
3005
+ unstable_dataStrategy?: DataStrategyFunction;
3006
+ } = {}
2932
3007
  ): Promise<StaticHandlerContext | Response> {
2933
3008
  let url = new URL(request.url);
2934
3009
  let method = request.method;
@@ -2974,7 +3049,15 @@ export function createStaticHandler(
2974
3049
  };
2975
3050
  }
2976
3051
 
2977
- let result = await queryImpl(request, location, matches, requestContext);
3052
+ let result = await queryImpl(
3053
+ request,
3054
+ location,
3055
+ matches,
3056
+ requestContext,
3057
+ unstable_dataStrategy || null,
3058
+ skipLoaderErrorBubbling === true,
3059
+ null
3060
+ );
2978
3061
  if (isResponse(result)) {
2979
3062
  return result;
2980
3063
  }
@@ -3004,6 +3087,12 @@ export function createStaticHandler(
3004
3087
  * serialize the error as they see fit while including the proper response
3005
3088
  * code. Examples here are 404 and 405 errors that occur prior to reaching
3006
3089
  * any user-defined loaders.
3090
+ *
3091
+ * - `opts.routeId` allows you to specify the specific route handler to call.
3092
+ * If not provided the handler will determine the proper route by matching
3093
+ * against `request.url`
3094
+ * - `opts.requestContext` is an optional server context that will be passed
3095
+ * to actions/loaders in the `context` parameter
3007
3096
  */
3008
3097
  async function queryRoute(
3009
3098
  request: Request,
@@ -3043,8 +3132,11 @@ export function createStaticHandler(
3043
3132
  location,
3044
3133
  matches,
3045
3134
  requestContext,
3135
+ null,
3136
+ false,
3046
3137
  match
3047
3138
  );
3139
+
3048
3140
  if (isResponse(result)) {
3049
3141
  return result;
3050
3142
  }
@@ -3079,7 +3171,9 @@ export function createStaticHandler(
3079
3171
  location: Location,
3080
3172
  matches: AgnosticDataRouteMatch[],
3081
3173
  requestContext: unknown,
3082
- routeMatch?: AgnosticDataRouteMatch
3174
+ unstable_dataStrategy: DataStrategyFunction | null,
3175
+ skipLoaderErrorBubbling: boolean,
3176
+ routeMatch: AgnosticDataRouteMatch | null
3083
3177
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
3084
3178
  invariant(
3085
3179
  request.signal,
@@ -3093,6 +3187,8 @@ export function createStaticHandler(
3093
3187
  matches,
3094
3188
  routeMatch || getTargetMatch(matches, location),
3095
3189
  requestContext,
3190
+ unstable_dataStrategy,
3191
+ skipLoaderErrorBubbling,
3096
3192
  routeMatch != null
3097
3193
  );
3098
3194
  return result;
@@ -3102,6 +3198,8 @@ export function createStaticHandler(
3102
3198
  request,
3103
3199
  matches,
3104
3200
  requestContext,
3201
+ unstable_dataStrategy,
3202
+ skipLoaderErrorBubbling,
3105
3203
  routeMatch
3106
3204
  );
3107
3205
  return isResponse(result)
@@ -3112,14 +3210,14 @@ export function createStaticHandler(
3112
3210
  actionHeaders: {},
3113
3211
  };
3114
3212
  } 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)) {
3213
+ // If the user threw/returned a Response in callLoaderOrAction for a
3214
+ // `queryRoute` call, we throw the `HandlerResult` to bail out early
3215
+ // and then return or throw the raw Response here accordingly
3216
+ if (isHandlerResult(e) && isResponse(e.result)) {
3119
3217
  if (e.type === ResultType.error) {
3120
- throw e.response;
3218
+ throw e.result;
3121
3219
  }
3122
- return e.response;
3220
+ return e.result;
3123
3221
  }
3124
3222
  // Redirects are always returned since they don't propagate to catch
3125
3223
  // boundaries
@@ -3135,6 +3233,8 @@ export function createStaticHandler(
3135
3233
  matches: AgnosticDataRouteMatch[],
3136
3234
  actionMatch: AgnosticDataRouteMatch,
3137
3235
  requestContext: unknown,
3236
+ unstable_dataStrategy: DataStrategyFunction | null,
3237
+ skipLoaderErrorBubbling: boolean,
3138
3238
  isRouteRequest: boolean
3139
3239
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
3140
3240
  let result: DataResult;
@@ -3153,17 +3253,16 @@ export function createStaticHandler(
3153
3253
  error,
3154
3254
  };
3155
3255
  } else {
3156
- result = await callLoaderOrAction(
3256
+ let results = await callDataStrategy(
3157
3257
  "action",
3158
3258
  request,
3159
- actionMatch,
3259
+ [actionMatch],
3160
3260
  matches,
3161
- manifest,
3162
- mapRouteProperties,
3163
- basename,
3164
- future.v7_relativeSplatPath,
3165
- { isStaticRequest: true, isRouteRequest, requestContext }
3261
+ isRouteRequest,
3262
+ requestContext,
3263
+ unstable_dataStrategy
3166
3264
  );
3265
+ result = results[0];
3167
3266
 
3168
3267
  if (request.signal.aborted) {
3169
3268
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
@@ -3176,9 +3275,9 @@ export function createStaticHandler(
3176
3275
  // can get back on the "throw all redirect responses" train here should
3177
3276
  // this ever happen :/
3178
3277
  throw new Response(null, {
3179
- status: result.status,
3278
+ status: result.response.status,
3180
3279
  headers: {
3181
- Location: result.location,
3280
+ Location: result.response.headers.get("Location")!,
3182
3281
  },
3183
3282
  });
3184
3283
  }
@@ -3215,18 +3314,28 @@ export function createStaticHandler(
3215
3314
  };
3216
3315
  }
3217
3316
 
3317
+ // Create a GET request for the loaders
3318
+ let loaderRequest = new Request(request.url, {
3319
+ headers: request.headers,
3320
+ redirect: request.redirect,
3321
+ signal: request.signal,
3322
+ });
3323
+
3218
3324
  if (isErrorResult(result)) {
3219
3325
  // Store off the pending error - we use it to determine which loaders
3220
3326
  // to call and will commit it when we complete the navigation
3221
- let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
3327
+ let boundaryMatch = skipLoaderErrorBubbling
3328
+ ? actionMatch
3329
+ : findNearestBoundary(matches, actionMatch.route.id);
3330
+
3222
3331
  let context = await loadRouteData(
3223
- request,
3332
+ loaderRequest,
3224
3333
  matches,
3225
3334
  requestContext,
3226
- undefined,
3227
- {
3228
- [boundaryMatch.route.id]: result.error,
3229
- }
3335
+ unstable_dataStrategy,
3336
+ skipLoaderErrorBubbling,
3337
+ null,
3338
+ [boundaryMatch.route.id, result]
3230
3339
  );
3231
3340
 
3232
3341
  // action status codes take precedence over loader status codes
@@ -3234,6 +3343,8 @@ export function createStaticHandler(
3234
3343
  ...context,
3235
3344
  statusCode: isRouteErrorResponse(result.error)
3236
3345
  ? result.error.status
3346
+ : result.statusCode != null
3347
+ ? result.statusCode
3237
3348
  : 500,
3238
3349
  actionData: null,
3239
3350
  actionHeaders: {
@@ -3242,24 +3353,25 @@ export function createStaticHandler(
3242
3353
  };
3243
3354
  }
3244
3355
 
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);
3356
+ let context = await loadRouteData(
3357
+ loaderRequest,
3358
+ matches,
3359
+ requestContext,
3360
+ unstable_dataStrategy,
3361
+ skipLoaderErrorBubbling,
3362
+ null
3363
+ );
3252
3364
 
3253
3365
  return {
3254
3366
  ...context,
3255
- // action status codes take precedence over loader status codes
3256
- ...(result.statusCode ? { statusCode: result.statusCode } : {}),
3257
3367
  actionData: {
3258
3368
  [actionMatch.route.id]: result.data,
3259
3369
  },
3260
- actionHeaders: {
3261
- ...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
3262
- },
3370
+ // action status codes take precedence over loader status codes
3371
+ ...(result.statusCode ? { statusCode: result.statusCode } : {}),
3372
+ actionHeaders: result.headers
3373
+ ? { [actionMatch.route.id]: result.headers }
3374
+ : {},
3263
3375
  };
3264
3376
  }
3265
3377
 
@@ -3267,8 +3379,10 @@ export function createStaticHandler(
3267
3379
  request: Request,
3268
3380
  matches: AgnosticDataRouteMatch[],
3269
3381
  requestContext: unknown,
3270
- routeMatch?: AgnosticDataRouteMatch,
3271
- pendingActionError?: RouteData
3382
+ unstable_dataStrategy: DataStrategyFunction | null,
3383
+ skipLoaderErrorBubbling: boolean,
3384
+ routeMatch: AgnosticDataRouteMatch | null,
3385
+ pendingActionResult?: PendingActionResult
3272
3386
  ): Promise<
3273
3387
  | Omit<
3274
3388
  StaticHandlerContext,
@@ -3293,10 +3407,9 @@ export function createStaticHandler(
3293
3407
 
3294
3408
  let requestMatches = routeMatch
3295
3409
  ? [routeMatch]
3296
- : getLoaderMatchesUntilBoundary(
3297
- matches,
3298
- Object.keys(pendingActionError || {})[0]
3299
- );
3410
+ : pendingActionResult && isErrorResult(pendingActionResult[1])
3411
+ ? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0])
3412
+ : matches;
3300
3413
  let matchesToLoad = requestMatches.filter(
3301
3414
  (m) => m.route.loader || m.route.lazy
3302
3415
  );
@@ -3310,28 +3423,27 @@ export function createStaticHandler(
3310
3423
  (acc, m) => Object.assign(acc, { [m.route.id]: null }),
3311
3424
  {}
3312
3425
  ),
3313
- errors: pendingActionError || null,
3426
+ errors:
3427
+ pendingActionResult && isErrorResult(pendingActionResult[1])
3428
+ ? {
3429
+ [pendingActionResult[0]]: pendingActionResult[1].error,
3430
+ }
3431
+ : null,
3314
3432
  statusCode: 200,
3315
3433
  loaderHeaders: {},
3316
3434
  activeDeferreds: null,
3317
3435
  };
3318
3436
  }
3319
3437
 
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
- ]);
3438
+ let results = await callDataStrategy(
3439
+ "loader",
3440
+ request,
3441
+ matchesToLoad,
3442
+ matches,
3443
+ isRouteRequest,
3444
+ requestContext,
3445
+ unstable_dataStrategy
3446
+ );
3335
3447
 
3336
3448
  if (request.signal.aborted) {
3337
3449
  throwStaticHandlerAbortedError(request, isRouteRequest, future);
@@ -3343,8 +3455,9 @@ export function createStaticHandler(
3343
3455
  matches,
3344
3456
  matchesToLoad,
3345
3457
  results,
3346
- pendingActionError,
3347
- activeDeferreds
3458
+ pendingActionResult,
3459
+ activeDeferreds,
3460
+ skipLoaderErrorBubbling
3348
3461
  );
3349
3462
 
3350
3463
  // Add a null for any non-loader matches for proper revalidation on the client
@@ -3367,6 +3480,53 @@ export function createStaticHandler(
3367
3480
  };
3368
3481
  }
3369
3482
 
3483
+ // Utility wrapper for calling dataStrategy server-side without having to
3484
+ // pass around the manifest, mapRouteProperties, etc.
3485
+ async function callDataStrategy(
3486
+ type: "loader" | "action",
3487
+ request: Request,
3488
+ matchesToLoad: AgnosticDataRouteMatch[],
3489
+ matches: AgnosticDataRouteMatch[],
3490
+ isRouteRequest: boolean,
3491
+ requestContext: unknown,
3492
+ unstable_dataStrategy: DataStrategyFunction | null
3493
+ ): Promise<DataResult[]> {
3494
+ let results = await callDataStrategyImpl(
3495
+ unstable_dataStrategy || defaultDataStrategy,
3496
+ type,
3497
+ request,
3498
+ matchesToLoad,
3499
+ matches,
3500
+ manifest,
3501
+ mapRouteProperties,
3502
+ requestContext
3503
+ );
3504
+
3505
+ return await Promise.all(
3506
+ results.map((result, i) => {
3507
+ if (isRedirectHandlerResult(result)) {
3508
+ let response = result.result as Response;
3509
+ // Throw redirects and let the server handle them with an HTTP redirect
3510
+ throw normalizeRelativeRoutingRedirectResponse(
3511
+ response,
3512
+ request,
3513
+ matchesToLoad[i].route.id,
3514
+ matches,
3515
+ basename,
3516
+ future.v7_relativeSplatPath
3517
+ );
3518
+ }
3519
+ if (isResponse(result.result) && isRouteRequest) {
3520
+ // For SSR single-route requests, we want to hand Responses back
3521
+ // directly without unwrapping
3522
+ throw result;
3523
+ }
3524
+
3525
+ return convertHandlerResultToDataResult(result);
3526
+ })
3527
+ );
3528
+ }
3529
+
3370
3530
  return {
3371
3531
  dataRoutes,
3372
3532
  query,
@@ -3643,7 +3803,7 @@ function normalizeNavigateOptions(
3643
3803
  // render so we don't need to load them
3644
3804
  function getLoaderMatchesUntilBoundary(
3645
3805
  matches: AgnosticDataRouteMatch[],
3646
- boundaryId?: string
3806
+ boundaryId: string
3647
3807
  ) {
3648
3808
  let boundaryMatches = matches;
3649
3809
  if (boundaryId) {
@@ -3662,6 +3822,7 @@ function getMatchesToLoad(
3662
3822
  submission: Submission | undefined,
3663
3823
  location: Location,
3664
3824
  isInitialLoad: boolean,
3825
+ skipActionErrorRevalidation: boolean,
3665
3826
  isRevalidationRequired: boolean,
3666
3827
  cancelledDeferredRoutes: string[],
3667
3828
  cancelledFetcherLoads: string[],
@@ -3670,21 +3831,33 @@ function getMatchesToLoad(
3670
3831
  fetchRedirectIds: Set<string>,
3671
3832
  routesToUse: AgnosticDataRouteObject[],
3672
3833
  basename: string | undefined,
3673
- pendingActionData?: RouteData,
3674
- pendingError?: RouteData
3834
+ pendingActionResult?: PendingActionResult
3675
3835
  ): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
3676
- let actionResult = pendingError
3677
- ? Object.values(pendingError)[0]
3678
- : pendingActionData
3679
- ? Object.values(pendingActionData)[0]
3836
+ let actionResult = pendingActionResult
3837
+ ? isErrorResult(pendingActionResult[1])
3838
+ ? pendingActionResult[1].error
3839
+ : pendingActionResult[1].data
3680
3840
  : undefined;
3681
-
3682
3841
  let currentUrl = history.createURL(state.location);
3683
3842
  let nextUrl = history.createURL(location);
3684
3843
 
3685
3844
  // 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);
3845
+ let boundaryId =
3846
+ pendingActionResult && isErrorResult(pendingActionResult[1])
3847
+ ? pendingActionResult[0]
3848
+ : undefined;
3849
+ let boundaryMatches = boundaryId
3850
+ ? getLoaderMatchesUntilBoundary(matches, boundaryId)
3851
+ : matches;
3852
+
3853
+ // Don't revalidate loaders by default after action 4xx/5xx responses
3854
+ // when the flag is enabled. They can still opt-into revalidation via
3855
+ // `shouldRevalidate` via `actionResult`
3856
+ let actionStatus = pendingActionResult
3857
+ ? pendingActionResult[1].statusCode
3858
+ : undefined;
3859
+ let shouldSkipRevalidation =
3860
+ skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
3688
3861
 
3689
3862
  let navigationMatches = boundaryMatches.filter((match, index) => {
3690
3863
  let { route } = match;
@@ -3698,7 +3871,7 @@ function getMatchesToLoad(
3698
3871
  }
3699
3872
 
3700
3873
  if (isInitialLoad) {
3701
- if (route.loader.hydrate) {
3874
+ if (typeof route.loader !== "function" || route.loader.hydrate) {
3702
3875
  return true;
3703
3876
  }
3704
3877
  return (
@@ -3730,15 +3903,16 @@ function getMatchesToLoad(
3730
3903
  nextParams: nextRouteMatch.params,
3731
3904
  ...submission,
3732
3905
  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),
3906
+ unstable_actionStatus: actionStatus,
3907
+ defaultShouldRevalidate: shouldSkipRevalidation
3908
+ ? false
3909
+ : // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3910
+ isRevalidationRequired ||
3911
+ currentUrl.pathname + currentUrl.search ===
3912
+ nextUrl.pathname + nextUrl.search ||
3913
+ // Search params affect all loaders
3914
+ currentUrl.search !== nextUrl.search ||
3915
+ isNewRouteInstance(currentRouteMatch, nextRouteMatch),
3742
3916
  });
3743
3917
  });
3744
3918
 
@@ -3808,7 +3982,10 @@ function getMatchesToLoad(
3808
3982
  nextParams: matches[matches.length - 1].params,
3809
3983
  ...submission,
3810
3984
  actionResult,
3811
- defaultShouldRevalidate: isRevalidationRequired,
3985
+ unstable_actionStatus: actionStatus,
3986
+ defaultShouldRevalidate: shouldSkipRevalidation
3987
+ ? false
3988
+ : isRevalidationRequired,
3812
3989
  });
3813
3990
  }
3814
3991
 
@@ -3954,39 +4131,138 @@ async function loadLazyRouteModule(
3954
4131
  });
3955
4132
  }
3956
4133
 
4134
+ // Default implementation of `dataStrategy` which fetches all loaders in parallel
4135
+ function defaultDataStrategy(
4136
+ opts: DataStrategyFunctionArgs
4137
+ ): ReturnType<DataStrategyFunction> {
4138
+ return Promise.all(opts.matches.map((m) => m.resolve()));
4139
+ }
4140
+
4141
+ async function callDataStrategyImpl(
4142
+ dataStrategyImpl: DataStrategyFunction,
4143
+ type: "loader" | "action",
4144
+ request: Request,
4145
+ matchesToLoad: AgnosticDataRouteMatch[],
4146
+ matches: AgnosticDataRouteMatch[],
4147
+ manifest: RouteManifest,
4148
+ mapRouteProperties: MapRoutePropertiesFunction,
4149
+ requestContext?: unknown
4150
+ ): Promise<HandlerResult[]> {
4151
+ let routeIdsToLoad = matchesToLoad.reduce(
4152
+ (acc, m) => acc.add(m.route.id),
4153
+ new Set<string>()
4154
+ );
4155
+ let loadedMatches = new Set<string>();
4156
+
4157
+ // Send all matches here to allow for a middleware-type implementation.
4158
+ // handler will be a no-op for unneeded routes and we filter those results
4159
+ // back out below.
4160
+ let results = await dataStrategyImpl({
4161
+ matches: matches.map((match) => {
4162
+ let shouldLoad = routeIdsToLoad.has(match.route.id);
4163
+ // `resolve` encapsulates the route.lazy, executing the
4164
+ // loader/action, and mapping return values/thrown errors to a
4165
+ // HandlerResult. Users can pass a callback to take fine-grained control
4166
+ // over the execution of the loader/action
4167
+ let resolve: DataStrategyMatch["resolve"] = (handlerOverride) => {
4168
+ loadedMatches.add(match.route.id);
4169
+ return shouldLoad
4170
+ ? callLoaderOrAction(
4171
+ type,
4172
+ request,
4173
+ match,
4174
+ manifest,
4175
+ mapRouteProperties,
4176
+ handlerOverride,
4177
+ requestContext
4178
+ )
4179
+ : Promise.resolve({ type: ResultType.data, result: undefined });
4180
+ };
4181
+
4182
+ return {
4183
+ ...match,
4184
+ shouldLoad,
4185
+ resolve,
4186
+ };
4187
+ }),
4188
+ request,
4189
+ params: matches[0].params,
4190
+ context: requestContext,
4191
+ });
4192
+
4193
+ // Throw if any loadRoute implementations not called since they are what
4194
+ // ensures a route is fully loaded
4195
+ matches.forEach((m) =>
4196
+ invariant(
4197
+ loadedMatches.has(m.route.id),
4198
+ `\`match.resolve()\` was not called for route id "${m.route.id}". ` +
4199
+ "You must call `match.resolve()` on every match passed to " +
4200
+ "`dataStrategy` to ensure all routes are properly loaded."
4201
+ )
4202
+ );
4203
+
4204
+ // Filter out any middleware-only matches for which we didn't need to run handlers
4205
+ return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
4206
+ }
4207
+
4208
+ // Default logic for calling a loader/action is the user has no specified a dataStrategy
3957
4209
  async function callLoaderOrAction(
3958
4210
  type: "loader" | "action",
3959
4211
  request: Request,
3960
4212
  match: AgnosticDataRouteMatch,
3961
- matches: AgnosticDataRouteMatch[],
3962
4213
  manifest: RouteManifest,
3963
4214
  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;
4215
+ handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
4216
+ staticContext?: unknown
4217
+ ): Promise<HandlerResult> {
4218
+ let result: HandlerResult;
3974
4219
  let onReject: (() => void) | undefined;
3975
4220
 
3976
- let runHandler = (handler: ActionFunction | LoaderFunction) => {
4221
+ let runHandler = (
4222
+ handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
4223
+ ): Promise<HandlerResult> => {
3977
4224
  // Setup a promise we can race against so that abort signals short circuit
3978
4225
  let reject: () => void;
3979
- let abortPromise = new Promise((_, r) => (reject = r));
4226
+ // This will never resolve so safe to type it as Promise<HandlerResult> to
4227
+ // satisfy the function return value
4228
+ let abortPromise = new Promise<HandlerResult>((_, r) => (reject = r));
3980
4229
  onReject = () => reject();
3981
4230
  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
- ]);
4231
+
4232
+ let actualHandler = (ctx?: unknown) => {
4233
+ if (typeof handler !== "function") {
4234
+ return Promise.reject(
4235
+ new Error(
4236
+ `You cannot call the handler for a route which defines a boolean ` +
4237
+ `"${type}" [routeId: ${match.route.id}]`
4238
+ )
4239
+ );
4240
+ }
4241
+ return handler(
4242
+ {
4243
+ request,
4244
+ params: match.params,
4245
+ context: staticContext,
4246
+ },
4247
+ ...(ctx !== undefined ? [ctx] : [])
4248
+ );
4249
+ };
4250
+
4251
+ let handlerPromise: Promise<HandlerResult>;
4252
+ if (handlerOverride) {
4253
+ handlerPromise = handlerOverride((ctx: unknown) => actualHandler(ctx));
4254
+ } else {
4255
+ handlerPromise = (async () => {
4256
+ try {
4257
+ let val = await actualHandler();
4258
+ return { type: "data", result: val };
4259
+ } catch (e) {
4260
+ return { type: "error", result: e };
4261
+ }
4262
+ })();
4263
+ }
4264
+
4265
+ return Promise.race([handlerPromise, abortPromise]);
3990
4266
  };
3991
4267
 
3992
4268
  try {
@@ -3996,7 +4272,7 @@ async function callLoaderOrAction(
3996
4272
  if (handler) {
3997
4273
  // Run statically defined handler in parallel with lazy()
3998
4274
  let handlerError;
3999
- let values = await Promise.all([
4275
+ let [value] = await Promise.all([
4000
4276
  // If the handler throws, don't let it immediately bubble out,
4001
4277
  // since we need to let the lazy() execution finish so we know if this
4002
4278
  // route has a boundary that can handle the error
@@ -4005,17 +4281,17 @@ async function callLoaderOrAction(
4005
4281
  }),
4006
4282
  loadLazyRouteModule(match.route, mapRouteProperties, manifest),
4007
4283
  ]);
4008
- if (handlerError) {
4284
+ if (handlerError !== undefined) {
4009
4285
  throw handlerError;
4010
4286
  }
4011
- result = values[0];
4287
+ result = value!;
4012
4288
  } else {
4013
4289
  // Load lazy route module, then run any returned handler
4014
4290
  await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
4015
4291
 
4016
4292
  handler = match.route[type];
4017
4293
  if (handler) {
4018
- // Handler still run even if we got interrupted to maintain consistency
4294
+ // Handler still runs even if we got interrupted to maintain consistency
4019
4295
  // with un-abortable behavior of handler execution on non-lazy or
4020
4296
  // previously-lazy-loaded routes
4021
4297
  result = await runHandler(handler);
@@ -4030,7 +4306,7 @@ async function callLoaderOrAction(
4030
4306
  } else {
4031
4307
  // lazy() route has no loader to run. Short circuit here so we don't
4032
4308
  // hit the invariant below that errors on returning undefined.
4033
- return { type: ResultType.data, data: undefined };
4309
+ return { type: ResultType.data, result: undefined };
4034
4310
  }
4035
4311
  }
4036
4312
  } else if (!handler) {
@@ -4044,85 +4320,31 @@ async function callLoaderOrAction(
4044
4320
  }
4045
4321
 
4046
4322
  invariant(
4047
- result !== undefined,
4323
+ result.result !== undefined,
4048
4324
  `You defined ${type === "action" ? "an action" : "a loader"} for route ` +
4049
4325
  `"${match.route.id}" but didn't return anything from your \`${type}\` ` +
4050
4326
  `function. Please return a value or \`null\`.`
4051
4327
  );
4052
4328
  } catch (e) {
4053
- resultType = ResultType.error;
4054
- result = e;
4329
+ // We should already be catching and converting normal handler executions to
4330
+ // HandlerResults and returning them, so anything that throws here is an
4331
+ // unexpected error we still need to wrap
4332
+ return { type: ResultType.error, result: e };
4055
4333
  } finally {
4056
4334
  if (onReject) {
4057
4335
  request.signal.removeEventListener("abort", onReject);
4058
4336
  }
4059
4337
  }
4060
4338
 
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
- }
4339
+ return result;
4340
+ }
4113
4341
 
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
- }
4342
+ async function convertHandlerResultToDataResult(
4343
+ handlerResult: HandlerResult
4344
+ ): Promise<DataResult> {
4345
+ let { result, type, status } = handlerResult;
4125
4346
 
4347
+ if (isResponse(result)) {
4126
4348
  let data: any;
4127
4349
 
4128
4350
  try {
@@ -4142,10 +4364,11 @@ async function callLoaderOrAction(
4142
4364
  return { type: ResultType.error, error: e };
4143
4365
  }
4144
4366
 
4145
- if (resultType === ResultType.error) {
4367
+ if (type === ResultType.error) {
4146
4368
  return {
4147
- type: resultType,
4148
- error: new ErrorResponseImpl(status, result.statusText, data),
4369
+ type: ResultType.error,
4370
+ error: new ErrorResponseImpl(result.status, result.statusText, data),
4371
+ statusCode: result.status,
4149
4372
  headers: result.headers,
4150
4373
  };
4151
4374
  }
@@ -4158,8 +4381,12 @@ async function callLoaderOrAction(
4158
4381
  };
4159
4382
  }
4160
4383
 
4161
- if (resultType === ResultType.error) {
4162
- return { type: resultType, error: result };
4384
+ if (type === ResultType.error) {
4385
+ return {
4386
+ type: ResultType.error,
4387
+ error: result,
4388
+ statusCode: isRouteErrorResponse(result) ? result.status : status,
4389
+ };
4163
4390
  }
4164
4391
 
4165
4392
  if (isDeferredData(result)) {
@@ -4171,7 +4398,60 @@ async function callLoaderOrAction(
4171
4398
  };
4172
4399
  }
4173
4400
 
4174
- return { type: ResultType.data, data: result };
4401
+ return { type: ResultType.data, data: result, statusCode: status };
4402
+ }
4403
+
4404
+ // Support relative routing in internal redirects
4405
+ function normalizeRelativeRoutingRedirectResponse(
4406
+ response: Response,
4407
+ request: Request,
4408
+ routeId: string,
4409
+ matches: AgnosticDataRouteMatch[],
4410
+ basename: string,
4411
+ v7_relativeSplatPath: boolean
4412
+ ) {
4413
+ let location = response.headers.get("Location");
4414
+ invariant(
4415
+ location,
4416
+ "Redirects returned/thrown from loaders/actions must have a Location header"
4417
+ );
4418
+
4419
+ if (!ABSOLUTE_URL_REGEX.test(location)) {
4420
+ let trimmedMatches = matches.slice(
4421
+ 0,
4422
+ matches.findIndex((m) => m.route.id === routeId) + 1
4423
+ );
4424
+ location = normalizeTo(
4425
+ new URL(request.url),
4426
+ trimmedMatches,
4427
+ basename,
4428
+ true,
4429
+ location,
4430
+ v7_relativeSplatPath
4431
+ );
4432
+ response.headers.set("Location", location);
4433
+ }
4434
+
4435
+ return response;
4436
+ }
4437
+
4438
+ function normalizeRedirectLocation(
4439
+ location: string,
4440
+ currentUrl: URL,
4441
+ basename: string
4442
+ ): string {
4443
+ if (ABSOLUTE_URL_REGEX.test(location)) {
4444
+ // Strip off the protocol+origin for same-origin + same-basename absolute redirects
4445
+ let normalizedLocation = location;
4446
+ let url = normalizedLocation.startsWith("//")
4447
+ ? new URL(currentUrl.protocol + normalizedLocation)
4448
+ : new URL(normalizedLocation);
4449
+ let isSameBasename = stripBasename(url.pathname, basename) != null;
4450
+ if (url.origin === currentUrl.origin && isSameBasename) {
4451
+ return url.pathname + url.search + url.hash;
4452
+ }
4453
+ }
4454
+ return location;
4175
4455
  }
4176
4456
 
4177
4457
  // Utility method for creating the Request instances for loaders/actions during
@@ -4239,8 +4519,9 @@ function processRouteLoaderData(
4239
4519
  matches: AgnosticDataRouteMatch[],
4240
4520
  matchesToLoad: AgnosticDataRouteMatch[],
4241
4521
  results: DataResult[],
4242
- pendingError: RouteData | undefined,
4243
- activeDeferreds: Map<string, DeferredData>
4522
+ pendingActionResult: PendingActionResult | undefined,
4523
+ activeDeferreds: Map<string, DeferredData>,
4524
+ skipLoaderErrorBubbling: boolean
4244
4525
  ): {
4245
4526
  loaderData: RouterState["loaderData"];
4246
4527
  errors: RouterState["errors"] | null;
@@ -4253,6 +4534,10 @@ function processRouteLoaderData(
4253
4534
  let statusCode: number | undefined;
4254
4535
  let foundError = false;
4255
4536
  let loaderHeaders: Record<string, Headers> = {};
4537
+ let pendingError =
4538
+ pendingActionResult && isErrorResult(pendingActionResult[1])
4539
+ ? pendingActionResult[1].error
4540
+ : undefined;
4256
4541
 
4257
4542
  // Process loader results into state.loaderData/state.errors
4258
4543
  results.forEach((result, index) => {
@@ -4262,23 +4547,27 @@ function processRouteLoaderData(
4262
4547
  "Cannot handle redirect results in processLoaderData"
4263
4548
  );
4264
4549
  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
4550
  let error = result.error;
4269
4551
  // If we have a pending action error, we report it at the highest-route
4270
4552
  // that throws a loader error, and then clear it out to indicate that
4271
4553
  // it was consumed
4272
- if (pendingError) {
4273
- error = Object.values(pendingError)[0];
4554
+ if (pendingError !== undefined) {
4555
+ error = pendingError;
4274
4556
  pendingError = undefined;
4275
4557
  }
4276
4558
 
4277
4559
  errors = errors || {};
4278
4560
 
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;
4561
+ if (skipLoaderErrorBubbling) {
4562
+ errors[id] = error;
4563
+ } else {
4564
+ // Look upwards from the matched route for the closest ancestor error
4565
+ // boundary, defaulting to the root match. Prefer higher error values
4566
+ // if lower errors bubble to the same boundary
4567
+ let boundaryMatch = findNearestBoundary(matches, id);
4568
+ if (errors[boundaryMatch.route.id] == null) {
4569
+ errors[boundaryMatch.route.id] = error;
4570
+ }
4282
4571
  }
4283
4572
 
4284
4573
  // Clear our any prior loaderData for the throwing route
@@ -4299,21 +4588,28 @@ function processRouteLoaderData(
4299
4588
  if (isDeferredResult(result)) {
4300
4589
  activeDeferreds.set(id, result.deferredData);
4301
4590
  loaderData[id] = result.deferredData.data;
4591
+ // Error status codes always override success status codes, but if all
4592
+ // loaders are successful we take the deepest status code.
4593
+ if (
4594
+ result.statusCode != null &&
4595
+ result.statusCode !== 200 &&
4596
+ !foundError
4597
+ ) {
4598
+ statusCode = result.statusCode;
4599
+ }
4600
+ if (result.headers) {
4601
+ loaderHeaders[id] = result.headers;
4602
+ }
4302
4603
  } else {
4303
4604
  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;
4605
+ // Error status codes always override success status codes, but if all
4606
+ // loaders are successful we take the deepest status code.
4607
+ if (result.statusCode && result.statusCode !== 200 && !foundError) {
4608
+ statusCode = result.statusCode;
4609
+ }
4610
+ if (result.headers) {
4611
+ loaderHeaders[id] = result.headers;
4612
+ }
4317
4613
  }
4318
4614
  }
4319
4615
  });
@@ -4321,9 +4617,9 @@ function processRouteLoaderData(
4321
4617
  // If we didn't consume the pending action error (i.e., all loaders
4322
4618
  // resolved), then consume it here. Also clear out any loaderData for the
4323
4619
  // throwing route
4324
- if (pendingError) {
4325
- errors = pendingError;
4326
- loaderData[Object.keys(pendingError)[0]] = undefined;
4620
+ if (pendingError !== undefined && pendingActionResult) {
4621
+ errors = { [pendingActionResult[0]]: pendingError };
4622
+ loaderData[pendingActionResult[0]] = undefined;
4327
4623
  }
4328
4624
 
4329
4625
  return {
@@ -4339,7 +4635,7 @@ function processLoaderData(
4339
4635
  matches: AgnosticDataRouteMatch[],
4340
4636
  matchesToLoad: AgnosticDataRouteMatch[],
4341
4637
  results: DataResult[],
4342
- pendingError: RouteData | undefined,
4638
+ pendingActionResult: PendingActionResult | undefined,
4343
4639
  revalidatingFetchers: RevalidatingFetcher[],
4344
4640
  fetcherResults: DataResult[],
4345
4641
  activeDeferreds: Map<string, DeferredData>
@@ -4351,8 +4647,9 @@ function processLoaderData(
4351
4647
  matches,
4352
4648
  matchesToLoad,
4353
4649
  results,
4354
- pendingError,
4355
- activeDeferreds
4650
+ pendingActionResult,
4651
+ activeDeferreds,
4652
+ false // This method is only called client side so we always want to bubble
4356
4653
  );
4357
4654
 
4358
4655
  // Process results from our revalidating fetchers
@@ -4425,6 +4722,24 @@ function mergeLoaderData(
4425
4722
  return mergedLoaderData;
4426
4723
  }
4427
4724
 
4725
+ function getActionDataForCommit(
4726
+ pendingActionResult: PendingActionResult | undefined
4727
+ ) {
4728
+ if (!pendingActionResult) {
4729
+ return {};
4730
+ }
4731
+ return isErrorResult(pendingActionResult[1])
4732
+ ? {
4733
+ // Clear out prior actionData on errors
4734
+ actionData: {},
4735
+ }
4736
+ : {
4737
+ actionData: {
4738
+ [pendingActionResult[0]]: pendingActionResult[1].data,
4739
+ },
4740
+ };
4741
+ }
4742
+
4428
4743
  // Find the nearest error boundary, looking upwards from the leaf route (or the
4429
4744
  // route specified by routeId) for the closest ancestor error boundary,
4430
4745
  // defaulting to the root match
@@ -4559,6 +4874,22 @@ function isHashChangeOnly(a: Location, b: Location): boolean {
4559
4874
  return false;
4560
4875
  }
4561
4876
 
4877
+ function isHandlerResult(result: unknown): result is HandlerResult {
4878
+ return (
4879
+ result != null &&
4880
+ typeof result === "object" &&
4881
+ "type" in result &&
4882
+ "result" in result &&
4883
+ (result.type === ResultType.data || result.type === ResultType.error)
4884
+ );
4885
+ }
4886
+
4887
+ function isRedirectHandlerResult(result: HandlerResult) {
4888
+ return (
4889
+ isResponse(result.result) && redirectStatusCodes.has(result.result.status)
4890
+ );
4891
+ }
4892
+
4562
4893
  function isDeferredResult(result: DataResult): result is DeferredResult {
4563
4894
  return result.type === ResultType.deferred;
4564
4895
  }
@@ -4603,14 +4934,6 @@ function isRedirectResponse(result: any): result is Response {
4603
4934
  return status >= 300 && status <= 399 && location != null;
4604
4935
  }
4605
4936
 
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
4937
  function isValidMethod(method: string): method is FormMethod | V7_FormMethod {
4615
4938
  return validRequestMethods.has(method.toLowerCase() as FormMethod);
4616
4939
  }