@remix-run/router 1.6.3 → 1.7.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
@@ -147,7 +147,7 @@ export interface Router {
147
147
  key: string,
148
148
  routeId: string,
149
149
  href: string | null,
150
- opts?: RouterNavigateOptions
150
+ opts?: RouterFetchOptions
151
151
  ): void;
152
152
 
153
153
  /**
@@ -419,41 +419,59 @@ export interface GetScrollPositionFunction {
419
419
 
420
420
  export type RelativeRoutingType = "route" | "path";
421
421
 
422
- type BaseNavigateOptions = {
423
- replace?: boolean;
424
- state?: any;
422
+ // Allowed for any navigation or fetch
423
+ type BaseNavigateOrFetchOptions = {
425
424
  preventScrollReset?: boolean;
426
425
  relative?: RelativeRoutingType;
426
+ };
427
+
428
+ // Only allowed for navigations
429
+ type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
430
+ replace?: boolean;
431
+ state?: any;
427
432
  fromRouteId?: string;
428
433
  };
429
434
 
435
+ // Only allowed for submission navigations
436
+ type BaseSubmissionOptions = {
437
+ formMethod?: HTMLFormMethod;
438
+ formEncType?: FormEncType;
439
+ } & (
440
+ | { formData: FormData; body?: undefined }
441
+ | { formData?: undefined; body: any }
442
+ );
443
+
430
444
  /**
431
- * Options for a navigate() call for a Link navigation
445
+ * Options for a navigate() call for a normal (non-submission) navigation
432
446
  */
433
447
  type LinkNavigateOptions = BaseNavigateOptions;
434
448
 
435
449
  /**
436
- * Options for a navigate() call for a Form navigation
450
+ * Options for a navigate() call for a submission navigation
437
451
  */
438
- type SubmissionNavigateOptions = BaseNavigateOptions & {
439
- formMethod?: HTMLFormMethod;
440
- formEncType?: FormEncType;
441
- formData: FormData;
442
- };
452
+ type SubmissionNavigateOptions = BaseNavigateOptions & BaseSubmissionOptions;
443
453
 
444
454
  /**
445
- * Options to pass to navigate() for either a Link or Form navigation
455
+ * Options to pass to navigate() for a navigation
446
456
  */
447
457
  export type RouterNavigateOptions =
448
458
  | LinkNavigateOptions
449
459
  | SubmissionNavigateOptions;
450
460
 
461
+ /**
462
+ * Options for a fetch() load
463
+ */
464
+ type LoadFetchOptions = BaseNavigateOrFetchOptions;
465
+
466
+ /**
467
+ * Options for a fetch() submission
468
+ */
469
+ type SubmitFetchOptions = BaseNavigateOrFetchOptions & BaseSubmissionOptions;
470
+
451
471
  /**
452
472
  * Options to pass to fetch()
453
473
  */
454
- export type RouterFetchOptions =
455
- | Omit<LinkNavigateOptions, "replace">
456
- | Omit<SubmissionNavigateOptions, "replace">;
474
+ export type RouterFetchOptions = LoadFetchOptions | SubmitFetchOptions;
457
475
 
458
476
  /**
459
477
  * Potential states for state.navigation
@@ -466,22 +484,28 @@ export type NavigationStates = {
466
484
  formAction: undefined;
467
485
  formEncType: undefined;
468
486
  formData: undefined;
487
+ json: undefined;
488
+ text: undefined;
469
489
  };
470
490
  Loading: {
471
491
  state: "loading";
472
492
  location: Location;
473
- formMethod: FormMethod | V7_FormMethod | undefined;
474
- formAction: string | undefined;
475
- formEncType: FormEncType | undefined;
476
- formData: FormData | undefined;
493
+ formMethod: Submission["formMethod"] | undefined;
494
+ formAction: Submission["formAction"] | undefined;
495
+ formEncType: Submission["formEncType"] | undefined;
496
+ formData: Submission["formData"] | undefined;
497
+ json: Submission["json"] | undefined;
498
+ text: Submission["text"] | undefined;
477
499
  };
478
500
  Submitting: {
479
501
  state: "submitting";
480
502
  location: Location;
481
- formMethod: FormMethod | V7_FormMethod;
482
- formAction: string;
483
- formEncType: FormEncType;
484
- formData: FormData;
503
+ formMethod: Submission["formMethod"];
504
+ formAction: Submission["formAction"];
505
+ formEncType: Submission["formEncType"];
506
+ formData: Submission["formData"];
507
+ json: Submission["json"];
508
+ text: Submission["text"];
485
509
  };
486
510
  };
487
511
 
@@ -498,25 +522,31 @@ type FetcherStates<TData = any> = {
498
522
  formMethod: undefined;
499
523
  formAction: undefined;
500
524
  formEncType: undefined;
525
+ text: undefined;
501
526
  formData: undefined;
527
+ json: undefined;
502
528
  data: TData | undefined;
503
529
  " _hasFetcherDoneAnything "?: boolean;
504
530
  };
505
531
  Loading: {
506
532
  state: "loading";
507
- formMethod: FormMethod | V7_FormMethod | undefined;
508
- formAction: string | undefined;
509
- formEncType: FormEncType | undefined;
510
- formData: FormData | undefined;
533
+ formMethod: Submission["formMethod"] | undefined;
534
+ formAction: Submission["formAction"] | undefined;
535
+ formEncType: Submission["formEncType"] | undefined;
536
+ text: Submission["text"] | undefined;
537
+ formData: Submission["formData"] | undefined;
538
+ json: Submission["json"] | undefined;
511
539
  data: TData | undefined;
512
540
  " _hasFetcherDoneAnything "?: boolean;
513
541
  };
514
542
  Submitting: {
515
543
  state: "submitting";
516
- formMethod: FormMethod | V7_FormMethod;
517
- formAction: string;
518
- formEncType: FormEncType;
519
- formData: FormData;
544
+ formMethod: Submission["formMethod"];
545
+ formAction: Submission["formAction"];
546
+ formEncType: Submission["formEncType"];
547
+ text: Submission["text"];
548
+ formData: Submission["formData"];
549
+ json: Submission["json"];
520
550
  data: TData | undefined;
521
551
  " _hasFetcherDoneAnything "?: boolean;
522
552
  };
@@ -642,6 +672,8 @@ export const IDLE_NAVIGATION: NavigationStates["Idle"] = {
642
672
  formAction: undefined,
643
673
  formEncType: undefined,
644
674
  formData: undefined,
675
+ json: undefined,
676
+ text: undefined,
645
677
  };
646
678
 
647
679
  export const IDLE_FETCHER: FetcherStates["Idle"] = {
@@ -651,6 +683,8 @@ export const IDLE_FETCHER: FetcherStates["Idle"] = {
651
683
  formAction: undefined,
652
684
  formEncType: undefined,
653
685
  formData: undefined,
686
+ json: undefined,
687
+ text: undefined,
654
688
  };
655
689
 
656
690
  export const IDLE_BLOCKER: BlockerUnblocked = {
@@ -893,8 +927,9 @@ export function createRouter(init: RouterInit): Router {
893
927
  init.history.go(delta);
894
928
  },
895
929
  reset() {
896
- deleteBlocker(blockerKey!);
897
- updateState({ blockers: new Map(router.state.blockers) });
930
+ let blockers = new Map(state.blockers);
931
+ blockers.set(blockerKey!, IDLE_BLOCKER);
932
+ updateState({ blockers });
898
933
  },
899
934
  });
900
935
  return;
@@ -991,9 +1026,8 @@ export function createRouter(init: RouterInit): Router {
991
1026
 
992
1027
  // On a successful navigation we can assume we got through all blockers
993
1028
  // so we can start fresh
994
- for (let [key] of blockerFunctions) {
995
- deleteBlocker(key);
996
- }
1029
+ let blockers = new Map();
1030
+ blockerFunctions.clear();
997
1031
 
998
1032
  // Always respect the user flag. Otherwise don't reset on mutation
999
1033
  // submission navigations unless they redirect
@@ -1008,6 +1042,16 @@ export function createRouter(init: RouterInit): Router {
1008
1042
  inFlightDataRoutes = undefined;
1009
1043
  }
1010
1044
 
1045
+ if (isUninterruptedRevalidation) {
1046
+ // If this was an uninterrupted revalidation then do not touch history
1047
+ } else if (pendingAction === HistoryAction.Pop) {
1048
+ // Do nothing for POP - URL has already been updated
1049
+ } else if (pendingAction === HistoryAction.Push) {
1050
+ init.history.push(location, location.state);
1051
+ } else if (pendingAction === HistoryAction.Replace) {
1052
+ init.history.replace(location, location.state);
1053
+ }
1054
+
1011
1055
  updateState({
1012
1056
  ...newState, // matches, errors, fetchers go through as-is
1013
1057
  actionData,
@@ -1022,19 +1066,9 @@ export function createRouter(init: RouterInit): Router {
1022
1066
  newState.matches || state.matches
1023
1067
  ),
1024
1068
  preventScrollReset,
1025
- blockers: new Map(state.blockers),
1069
+ blockers,
1026
1070
  });
1027
1071
 
1028
- if (isUninterruptedRevalidation) {
1029
- // If this was an uninterrupted revalidation then do not touch history
1030
- } else if (pendingAction === HistoryAction.Pop) {
1031
- // Do nothing for POP - URL has already been updated
1032
- } else if (pendingAction === HistoryAction.Push) {
1033
- init.history.push(location, location.state);
1034
- } else if (pendingAction === HistoryAction.Replace) {
1035
- init.history.replace(location, location.state);
1036
- }
1037
-
1038
1072
  // Reset stateful navigation vars
1039
1073
  pendingAction = HistoryAction.Pop;
1040
1074
  pendingPreventScrollReset = false;
@@ -1114,6 +1148,7 @@ export function createRouter(init: RouterInit): Router {
1114
1148
  nextLocation,
1115
1149
  historyAction,
1116
1150
  });
1151
+
1117
1152
  if (blockerKey) {
1118
1153
  // Put the blocker into a blocked state
1119
1154
  updateBlocker(blockerKey, {
@@ -1130,8 +1165,9 @@ export function createRouter(init: RouterInit): Router {
1130
1165
  navigate(to, opts);
1131
1166
  },
1132
1167
  reset() {
1133
- deleteBlocker(blockerKey!);
1134
- updateState({ blockers: new Map(state.blockers) });
1168
+ let blockers = new Map(state.blockers);
1169
+ blockers.set(blockerKey!, IDLE_BLOCKER);
1170
+ updateState({ blockers });
1135
1171
  },
1136
1172
  });
1137
1173
  return;
@@ -1286,13 +1322,7 @@ export function createRouter(init: RouterInit): Router {
1286
1322
 
1287
1323
  pendingActionData = actionOutput.pendingActionData;
1288
1324
  pendingError = actionOutput.pendingActionError;
1289
-
1290
- let navigation: NavigationStates["Loading"] = {
1291
- state: "loading",
1292
- location,
1293
- ...opts.submission,
1294
- };
1295
- loadingNavigation = navigation;
1325
+ loadingNavigation = getLoadingNavigation(location, opts.submission);
1296
1326
 
1297
1327
  // Create a GET request for the loaders
1298
1328
  request = new Request(request.url, { signal: request.signal });
@@ -1335,16 +1365,12 @@ export function createRouter(init: RouterInit): Router {
1335
1365
  location: Location,
1336
1366
  submission: Submission,
1337
1367
  matches: AgnosticDataRouteMatch[],
1338
- opts?: { replace?: boolean }
1368
+ opts: { replace?: boolean } = {}
1339
1369
  ): Promise<HandleActionResult> {
1340
1370
  interruptActiveLoads();
1341
1371
 
1342
1372
  // Put us in a submitting state
1343
- let navigation: NavigationStates["Submitting"] = {
1344
- state: "submitting",
1345
- location,
1346
- ...submission,
1347
- };
1373
+ let navigation = getSubmittingNavigation(location, submission);
1348
1374
  updateState({ navigation });
1349
1375
 
1350
1376
  // Call our action and get the result
@@ -1434,36 +1460,15 @@ export function createRouter(init: RouterInit): Router {
1434
1460
  pendingError?: RouteData
1435
1461
  ): Promise<HandleLoadersResult> {
1436
1462
  // Figure out the right navigation we want to use for data loading
1437
- let loadingNavigation = overrideNavigation;
1438
- if (!loadingNavigation) {
1439
- let navigation: NavigationStates["Loading"] = {
1440
- state: "loading",
1441
- location,
1442
- formMethod: undefined,
1443
- formAction: undefined,
1444
- formEncType: undefined,
1445
- formData: undefined,
1446
- ...submission,
1447
- };
1448
- loadingNavigation = navigation;
1449
- }
1463
+ let loadingNavigation =
1464
+ overrideNavigation || getLoadingNavigation(location, submission);
1450
1465
 
1451
1466
  // If this was a redirect from an action we don't have a "submission" but
1452
1467
  // we have it on the loading navigation so use that if available
1453
1468
  let activeSubmission =
1454
- submission || fetcherSubmission
1455
- ? submission || fetcherSubmission
1456
- : loadingNavigation.formMethod &&
1457
- loadingNavigation.formAction &&
1458
- loadingNavigation.formData &&
1459
- loadingNavigation.formEncType
1460
- ? {
1461
- formMethod: loadingNavigation.formMethod,
1462
- formAction: loadingNavigation.formAction,
1463
- formData: loadingNavigation.formData,
1464
- formEncType: loadingNavigation.formEncType,
1465
- }
1466
- : undefined;
1469
+ submission ||
1470
+ fetcherSubmission ||
1471
+ getSubmissionFromNavigation(loadingNavigation);
1467
1472
 
1468
1473
  let routesToUse = inFlightDataRoutes || dataRoutes;
1469
1474
  let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
@@ -1476,6 +1481,7 @@ export function createRouter(init: RouterInit): Router {
1476
1481
  cancelledDeferredRoutes,
1477
1482
  cancelledFetcherLoads,
1478
1483
  fetchLoadMatches,
1484
+ fetchRedirectIds,
1479
1485
  routesToUse,
1480
1486
  basename,
1481
1487
  pendingActionData,
@@ -1512,15 +1518,10 @@ export function createRouter(init: RouterInit): Router {
1512
1518
  if (!isUninterruptedRevalidation) {
1513
1519
  revalidatingFetchers.forEach((rf) => {
1514
1520
  let fetcher = state.fetchers.get(rf.key);
1515
- let revalidatingFetcher: FetcherStates["Loading"] = {
1516
- state: "loading",
1517
- data: fetcher && fetcher.data,
1518
- formMethod: undefined,
1519
- formAction: undefined,
1520
- formEncType: undefined,
1521
- formData: undefined,
1522
- " _hasFetcherDoneAnything ": true,
1523
- };
1521
+ let revalidatingFetcher = getLoadingFetcher(
1522
+ undefined,
1523
+ fetcher ? fetcher.data : undefined
1524
+ );
1524
1525
  state.fetchers.set(rf.key, revalidatingFetcher);
1525
1526
  });
1526
1527
  let actionData = pendingActionData || state.actionData;
@@ -1539,6 +1540,9 @@ export function createRouter(init: RouterInit): Router {
1539
1540
 
1540
1541
  pendingNavigationLoadId = ++incrementingLoadId;
1541
1542
  revalidatingFetchers.forEach((rf) => {
1543
+ if (fetchControllers.has(rf.key)) {
1544
+ abortFetcher(rf.key);
1545
+ }
1542
1546
  if (rf.controller) {
1543
1547
  // Fetchers use an independent AbortController so that aborting a fetcher
1544
1548
  // (via deleteFetcher) does not abort the triggering navigation that
@@ -1666,12 +1670,18 @@ export function createRouter(init: RouterInit): Router {
1666
1670
  return;
1667
1671
  }
1668
1672
 
1669
- let { path, submission } = normalizeNavigateOptions(
1673
+ let { path, submission, error } = normalizeNavigateOptions(
1670
1674
  future.v7_normalizeFormMethod,
1671
1675
  true,
1672
1676
  normalizedPath,
1673
1677
  opts
1674
1678
  );
1679
+
1680
+ if (error) {
1681
+ setFetcherError(key, routeId, error);
1682
+ return;
1683
+ }
1684
+
1675
1685
  let match = getTargetMatch(matches, path);
1676
1686
 
1677
1687
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
@@ -1712,12 +1722,7 @@ export function createRouter(init: RouterInit): Router {
1712
1722
 
1713
1723
  // Put this fetcher into it's submitting state
1714
1724
  let existingFetcher = state.fetchers.get(key);
1715
- let fetcher: FetcherStates["Submitting"] = {
1716
- state: "submitting",
1717
- ...submission,
1718
- data: existingFetcher && existingFetcher.data,
1719
- " _hasFetcherDoneAnything ": true,
1720
- };
1725
+ let fetcher = getSubmittingFetcher(submission, existingFetcher);
1721
1726
  state.fetchers.set(key, fetcher);
1722
1727
  updateState({ fetchers: new Map(state.fetchers) });
1723
1728
 
@@ -1753,12 +1758,7 @@ export function createRouter(init: RouterInit): Router {
1753
1758
  if (isRedirectResult(actionResult)) {
1754
1759
  fetchControllers.delete(key);
1755
1760
  fetchRedirectIds.add(key);
1756
- let loadingFetcher: FetcherStates["Loading"] = {
1757
- state: "loading",
1758
- ...submission,
1759
- data: undefined,
1760
- " _hasFetcherDoneAnything ": true,
1761
- };
1761
+ let loadingFetcher = getLoadingFetcher(submission);
1762
1762
  state.fetchers.set(key, loadingFetcher);
1763
1763
  updateState({ fetchers: new Map(state.fetchers) });
1764
1764
 
@@ -1797,12 +1797,7 @@ export function createRouter(init: RouterInit): Router {
1797
1797
  let loadId = ++incrementingLoadId;
1798
1798
  fetchReloadIds.set(key, loadId);
1799
1799
 
1800
- let loadFetcher: FetcherStates["Loading"] = {
1801
- state: "loading",
1802
- data: actionResult.data,
1803
- ...submission,
1804
- " _hasFetcherDoneAnything ": true,
1805
- };
1800
+ let loadFetcher = getLoadingFetcher(submission, actionResult.data);
1806
1801
  state.fetchers.set(key, loadFetcher);
1807
1802
 
1808
1803
  let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
@@ -1815,6 +1810,7 @@ export function createRouter(init: RouterInit): Router {
1815
1810
  cancelledDeferredRoutes,
1816
1811
  cancelledFetcherLoads,
1817
1812
  fetchLoadMatches,
1813
+ fetchRedirectIds,
1818
1814
  routesToUse,
1819
1815
  basename,
1820
1816
  { [match.route.id]: actionResult.data },
@@ -1829,16 +1825,14 @@ export function createRouter(init: RouterInit): Router {
1829
1825
  .forEach((rf) => {
1830
1826
  let staleKey = rf.key;
1831
1827
  let existingFetcher = state.fetchers.get(staleKey);
1832
- let revalidatingFetcher: FetcherStates["Loading"] = {
1833
- state: "loading",
1834
- data: existingFetcher && existingFetcher.data,
1835
- formMethod: undefined,
1836
- formAction: undefined,
1837
- formEncType: undefined,
1838
- formData: undefined,
1839
- " _hasFetcherDoneAnything ": true,
1840
- };
1828
+ let revalidatingFetcher = getLoadingFetcher(
1829
+ undefined,
1830
+ existingFetcher ? existingFetcher.data : undefined
1831
+ );
1841
1832
  state.fetchers.set(staleKey, revalidatingFetcher);
1833
+ if (fetchControllers.has(staleKey)) {
1834
+ abortFetcher(staleKey);
1835
+ }
1842
1836
  if (rf.controller) {
1843
1837
  fetchControllers.set(staleKey, rf.controller);
1844
1838
  }
@@ -1896,15 +1890,7 @@ export function createRouter(init: RouterInit): Router {
1896
1890
  // Since we let revalidations complete even if the submitting fetcher was
1897
1891
  // deleted, only put it back to idle if it hasn't been deleted
1898
1892
  if (state.fetchers.has(key)) {
1899
- let doneFetcher: FetcherStates["Idle"] = {
1900
- state: "idle",
1901
- data: actionResult.data,
1902
- formMethod: undefined,
1903
- formAction: undefined,
1904
- formEncType: undefined,
1905
- formData: undefined,
1906
- " _hasFetcherDoneAnything ": true,
1907
- };
1893
+ let doneFetcher = getDoneFetcher(actionResult.data);
1908
1894
  state.fetchers.set(key, doneFetcher);
1909
1895
  }
1910
1896
 
@@ -1957,16 +1943,10 @@ export function createRouter(init: RouterInit): Router {
1957
1943
  ) {
1958
1944
  let existingFetcher = state.fetchers.get(key);
1959
1945
  // Put this fetcher into it's loading state
1960
- let loadingFetcher: FetcherStates["Loading"] = {
1961
- state: "loading",
1962
- formMethod: undefined,
1963
- formAction: undefined,
1964
- formEncType: undefined,
1965
- formData: undefined,
1966
- ...submission,
1967
- data: existingFetcher && existingFetcher.data,
1968
- " _hasFetcherDoneAnything ": true,
1969
- };
1946
+ let loadingFetcher = getLoadingFetcher(
1947
+ submission,
1948
+ existingFetcher ? existingFetcher.data : undefined
1949
+ );
1970
1950
  state.fetchers.set(key, loadingFetcher);
1971
1951
  updateState({ fetchers: new Map(state.fetchers) });
1972
1952
 
@@ -2035,15 +2015,7 @@ export function createRouter(init: RouterInit): Router {
2035
2015
  invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
2036
2016
 
2037
2017
  // Put the fetcher back into an idle state
2038
- let doneFetcher: FetcherStates["Idle"] = {
2039
- state: "idle",
2040
- data: result.data,
2041
- formMethod: undefined,
2042
- formAction: undefined,
2043
- formEncType: undefined,
2044
- formData: undefined,
2045
- " _hasFetcherDoneAnything ": true,
2046
- };
2018
+ let doneFetcher = getDoneFetcher(result.data);
2047
2019
  state.fetchers.set(key, doneFetcher);
2048
2020
  updateState({ fetchers: new Map(state.fetchers) });
2049
2021
  }
@@ -2121,27 +2093,20 @@ export function createRouter(init: RouterInit): Router {
2121
2093
 
2122
2094
  // Use the incoming submission if provided, fallback on the active one in
2123
2095
  // state.navigation
2124
- let { formMethod, formAction, formEncType, formData } = state.navigation;
2125
- if (!submission && formMethod && formAction && formData && formEncType) {
2126
- submission = {
2127
- formMethod,
2128
- formAction,
2129
- formEncType,
2130
- formData,
2131
- };
2132
- }
2096
+ let activeSubmission =
2097
+ submission || getSubmissionFromNavigation(state.navigation);
2133
2098
 
2134
2099
  // If this was a 307/308 submission we want to preserve the HTTP method and
2135
2100
  // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2136
2101
  // redirected location
2137
2102
  if (
2138
2103
  redirectPreserveMethodStatusCodes.has(redirect.status) &&
2139
- submission &&
2140
- isMutationMethod(submission.formMethod)
2104
+ activeSubmission &&
2105
+ isMutationMethod(activeSubmission.formMethod)
2141
2106
  ) {
2142
2107
  await startNavigation(redirectHistoryAction, redirectLocation, {
2143
2108
  submission: {
2144
- ...submission,
2109
+ ...activeSubmission,
2145
2110
  formAction: redirect.location,
2146
2111
  },
2147
2112
  // Preserve this flag across redirects
@@ -2151,30 +2116,19 @@ export function createRouter(init: RouterInit): Router {
2151
2116
  // For a fetch action redirect, we kick off a new loading navigation
2152
2117
  // without the fetcher submission, but we send it along for shouldRevalidate
2153
2118
  await startNavigation(redirectHistoryAction, redirectLocation, {
2154
- overrideNavigation: {
2155
- state: "loading",
2156
- location: redirectLocation,
2157
- formMethod: undefined,
2158
- formAction: undefined,
2159
- formEncType: undefined,
2160
- formData: undefined,
2161
- },
2162
- fetcherSubmission: submission,
2119
+ overrideNavigation: getLoadingNavigation(redirectLocation),
2120
+ fetcherSubmission: activeSubmission,
2163
2121
  // Preserve this flag across redirects
2164
2122
  preventScrollReset: pendingPreventScrollReset,
2165
2123
  });
2166
2124
  } else {
2167
- // Otherwise, we kick off a new loading navigation, preserving the
2168
- // submission info for the duration of this navigation
2125
+ // If we have a submission, we will preserve it through the redirect navigation
2126
+ let overrideNavigation = getLoadingNavigation(
2127
+ redirectLocation,
2128
+ activeSubmission
2129
+ );
2169
2130
  await startNavigation(redirectHistoryAction, redirectLocation, {
2170
- overrideNavigation: {
2171
- state: "loading",
2172
- location: redirectLocation,
2173
- formMethod: submission ? submission.formMethod : undefined,
2174
- formAction: submission ? submission.formAction : undefined,
2175
- formEncType: submission ? submission.formEncType : undefined,
2176
- formData: submission ? submission.formData : undefined,
2177
- },
2131
+ overrideNavigation,
2178
2132
  // Preserve this flag across redirects
2179
2133
  preventScrollReset: pendingPreventScrollReset,
2180
2134
  });
@@ -2302,15 +2256,7 @@ export function createRouter(init: RouterInit): Router {
2302
2256
  function markFetchersDone(keys: string[]) {
2303
2257
  for (let key of keys) {
2304
2258
  let fetcher = getFetcher(key);
2305
- let doneFetcher: FetcherStates["Idle"] = {
2306
- state: "idle",
2307
- data: fetcher.data,
2308
- formMethod: undefined,
2309
- formAction: undefined,
2310
- formEncType: undefined,
2311
- formData: undefined,
2312
- " _hasFetcherDoneAnything ": true,
2313
- };
2259
+ let doneFetcher = getDoneFetcher(fetcher.data);
2314
2260
  state.fetchers.set(key, doneFetcher);
2315
2261
  }
2316
2262
  }
@@ -2378,8 +2324,9 @@ export function createRouter(init: RouterInit): Router {
2378
2324
  `Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`
2379
2325
  );
2380
2326
 
2381
- state.blockers.set(key, newBlocker);
2382
- updateState({ blockers: new Map(state.blockers) });
2327
+ let blockers = new Map(state.blockers);
2328
+ blockers.set(key, newBlocker);
2329
+ updateState({ blockers });
2383
2330
  }
2384
2331
 
2385
2332
  function shouldBlockNavigation({
@@ -2444,7 +2391,7 @@ export function createRouter(init: RouterInit): Router {
2444
2391
  ) {
2445
2392
  savedScrollPositions = positions;
2446
2393
  getScrollPosition = getPosition;
2447
- getScrollRestorationKey = getKey || ((location) => location.key);
2394
+ getScrollRestorationKey = getKey || null;
2448
2395
 
2449
2396
  // Perform initial hydration scroll restoration, since we miss the boat on
2450
2397
  // the initial updateState() because we've not yet rendered <ScrollRestoration/>
@@ -2464,15 +2411,23 @@ export function createRouter(init: RouterInit): Router {
2464
2411
  };
2465
2412
  }
2466
2413
 
2414
+ function getScrollKey(location: Location, matches: AgnosticDataRouteMatch[]) {
2415
+ if (getScrollRestorationKey) {
2416
+ let key = getScrollRestorationKey(
2417
+ location,
2418
+ matches.map((m) => createUseMatchesMatch(m, state.loaderData))
2419
+ );
2420
+ return key || location.key;
2421
+ }
2422
+ return location.key;
2423
+ }
2424
+
2467
2425
  function saveScrollPosition(
2468
2426
  location: Location,
2469
2427
  matches: AgnosticDataRouteMatch[]
2470
2428
  ): void {
2471
- if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
2472
- let userMatches = matches.map((m) =>
2473
- createUseMatchesMatch(m, state.loaderData)
2474
- );
2475
- let key = getScrollRestorationKey(location, userMatches) || location.key;
2429
+ if (savedScrollPositions && getScrollPosition) {
2430
+ let key = getScrollKey(location, matches);
2476
2431
  savedScrollPositions[key] = getScrollPosition();
2477
2432
  }
2478
2433
  }
@@ -2481,11 +2436,8 @@ export function createRouter(init: RouterInit): Router {
2481
2436
  location: Location,
2482
2437
  matches: AgnosticDataRouteMatch[]
2483
2438
  ): number | null {
2484
- if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
2485
- let userMatches = matches.map((m) =>
2486
- createUseMatchesMatch(m, state.loaderData)
2487
- );
2488
- let key = getScrollRestorationKey(location, userMatches) || location.key;
2439
+ if (savedScrollPositions) {
2440
+ let key = getScrollKey(location, matches);
2489
2441
  let y = savedScrollPositions[key];
2490
2442
  if (typeof y === "number") {
2491
2443
  return y;
@@ -2840,9 +2792,7 @@ export function createStaticHandler(
2840
2792
  manifest,
2841
2793
  mapRouteProperties,
2842
2794
  basename,
2843
- true,
2844
- isRouteRequest,
2845
- requestContext
2795
+ { isStaticRequest: true, isRouteRequest, requestContext }
2846
2796
  );
2847
2797
 
2848
2798
  if (request.signal.aborted) {
@@ -3008,9 +2958,7 @@ export function createStaticHandler(
3008
2958
  manifest,
3009
2959
  mapRouteProperties,
3010
2960
  basename,
3011
- true,
3012
- isRouteRequest,
3013
- requestContext
2961
+ { isStaticRequest: true, isRouteRequest, requestContext }
3014
2962
  )
3015
2963
  ),
3016
2964
  ]);
@@ -3085,7 +3033,11 @@ export function getStaticContextFromError(
3085
3033
  function isSubmissionNavigation(
3086
3034
  opts: RouterNavigateOptions
3087
3035
  ): opts is SubmissionNavigateOptions {
3088
- return opts != null && "formData" in opts;
3036
+ return (
3037
+ opts != null &&
3038
+ (("formData" in opts && opts.formData != null) ||
3039
+ ("body" in opts && opts.body !== undefined))
3040
+ );
3089
3041
  }
3090
3042
 
3091
3043
  function normalizeTo(
@@ -3181,28 +3133,120 @@ function normalizeNavigateOptions(
3181
3133
  };
3182
3134
  }
3183
3135
 
3136
+ let getInvalidBodyError = () => ({
3137
+ path,
3138
+ error: getInternalRouterError(400, { type: "invalid-body" }),
3139
+ });
3140
+
3184
3141
  // Create a Submission on non-GET navigations
3185
- let submission: Submission | undefined;
3186
- if (opts.formData) {
3187
- let formMethod = opts.formMethod || "get";
3188
- submission = {
3189
- formMethod: normalizeFormMethod
3190
- ? (formMethod.toUpperCase() as V7_FormMethod)
3191
- : (formMethod.toLowerCase() as FormMethod),
3192
- formAction: stripHashFromPath(path),
3193
- formEncType:
3194
- (opts && opts.formEncType) || "application/x-www-form-urlencoded",
3195
- formData: opts.formData,
3196
- };
3142
+ let rawFormMethod = opts.formMethod || "get";
3143
+ let formMethod = normalizeFormMethod
3144
+ ? (rawFormMethod.toUpperCase() as V7_FormMethod)
3145
+ : (rawFormMethod.toLowerCase() as FormMethod);
3146
+ let formAction = stripHashFromPath(path);
3147
+
3148
+ if (opts.body !== undefined) {
3149
+ if (opts.formEncType === "text/plain") {
3150
+ // text only support POST/PUT/PATCH/DELETE submissions
3151
+ if (!isMutationMethod(formMethod)) {
3152
+ return getInvalidBodyError();
3153
+ }
3154
+
3155
+ let text =
3156
+ typeof opts.body === "string"
3157
+ ? opts.body
3158
+ : opts.body instanceof FormData ||
3159
+ opts.body instanceof URLSearchParams
3160
+ ? // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
3161
+ Array.from(opts.body.entries()).reduce(
3162
+ (acc, [name, value]) => `${acc}${name}=${value}\n`,
3163
+ ""
3164
+ )
3165
+ : String(opts.body);
3197
3166
 
3198
- if (isMutationMethod(submission.formMethod)) {
3199
- return { path, submission };
3167
+ return {
3168
+ path,
3169
+ submission: {
3170
+ formMethod,
3171
+ formAction,
3172
+ formEncType: opts.formEncType,
3173
+ formData: undefined,
3174
+ json: undefined,
3175
+ text,
3176
+ },
3177
+ };
3178
+ } else if (opts.formEncType === "application/json") {
3179
+ // json only supports POST/PUT/PATCH/DELETE submissions
3180
+ if (!isMutationMethod(formMethod)) {
3181
+ return getInvalidBodyError();
3182
+ }
3183
+
3184
+ try {
3185
+ let json =
3186
+ typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;
3187
+
3188
+ return {
3189
+ path,
3190
+ submission: {
3191
+ formMethod,
3192
+ formAction,
3193
+ formEncType: opts.formEncType,
3194
+ formData: undefined,
3195
+ json,
3196
+ text: undefined,
3197
+ },
3198
+ };
3199
+ } catch (e) {
3200
+ return getInvalidBodyError();
3201
+ }
3200
3202
  }
3201
3203
  }
3202
3204
 
3205
+ invariant(
3206
+ typeof FormData === "function",
3207
+ "FormData is not available in this environment"
3208
+ );
3209
+
3210
+ let searchParams: URLSearchParams;
3211
+ let formData: FormData;
3212
+
3213
+ if (opts.formData) {
3214
+ searchParams = convertFormDataToSearchParams(opts.formData);
3215
+ formData = opts.formData;
3216
+ } else if (opts.body instanceof FormData) {
3217
+ searchParams = convertFormDataToSearchParams(opts.body);
3218
+ formData = opts.body;
3219
+ } else if (opts.body instanceof URLSearchParams) {
3220
+ searchParams = opts.body;
3221
+ formData = convertSearchParamsToFormData(searchParams);
3222
+ } else if (opts.body == null) {
3223
+ searchParams = new URLSearchParams();
3224
+ formData = new FormData();
3225
+ } else {
3226
+ try {
3227
+ searchParams = new URLSearchParams(opts.body);
3228
+ formData = convertSearchParamsToFormData(searchParams);
3229
+ } catch (e) {
3230
+ return getInvalidBodyError();
3231
+ }
3232
+ }
3233
+
3234
+ let submission: Submission = {
3235
+ formMethod,
3236
+ formAction,
3237
+ formEncType:
3238
+ (opts && opts.formEncType) || "application/x-www-form-urlencoded",
3239
+ formData,
3240
+ json: undefined,
3241
+ text: undefined,
3242
+ };
3243
+
3244
+ if (isMutationMethod(submission.formMethod)) {
3245
+ return { path, submission };
3246
+ }
3247
+
3203
3248
  // Flatten submission onto URLSearchParams for GET submissions
3204
3249
  let parsedPath = parsePath(path);
3205
- let searchParams = convertFormDataToSearchParams(opts.formData);
3206
3250
  // On GET navigation submissions we can drop the ?index param from the
3207
3251
  // resulting location since all loaders will run. But fetcher GET submissions
3208
3252
  // only run a single loader so we need to preserve any incoming ?index params
@@ -3240,6 +3284,7 @@ function getMatchesToLoad(
3240
3284
  cancelledDeferredRoutes: string[],
3241
3285
  cancelledFetcherLoads: string[],
3242
3286
  fetchLoadMatches: Map<string, FetchLoadMatch>,
3287
+ fetchRedirectIds: Set<string>,
3243
3288
  routesToUse: AgnosticDataRouteObject[],
3244
3289
  basename: string | undefined,
3245
3290
  pendingActionData?: RouteData,
@@ -3325,34 +3370,38 @@ function getMatchesToLoad(
3325
3370
  return;
3326
3371
  }
3327
3372
 
3373
+ // Revalidating fetchers are decoupled from the route matches since they
3374
+ // load from a static href. They only set `defaultShouldRevalidate` on
3375
+ // explicit revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3376
+ //
3377
+ // They automatically revalidate without even calling shouldRevalidate if:
3378
+ // - They were cancelled
3379
+ // - They're in the middle of their first load and therefore this is still
3380
+ // an initial load and not a revalidation
3381
+ //
3382
+ // If neither of those is true, then they _always_ check shouldRevalidate
3383
+ let fetcher = state.fetchers.get(key);
3384
+ let isPerformingInitialLoad =
3385
+ fetcher &&
3386
+ fetcher.state !== "idle" &&
3387
+ fetcher.data === undefined &&
3388
+ // If a fetcher.load redirected then it'll be "loading" without any data
3389
+ // so ensure we're not processing the redirect from this fetcher
3390
+ !fetchRedirectIds.has(key);
3328
3391
  let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3329
-
3330
- if (cancelledFetcherLoads.includes(key)) {
3331
- revalidatingFetchers.push({
3332
- key,
3333
- routeId: f.routeId,
3334
- path: f.path,
3335
- matches: fetcherMatches,
3336
- match: fetcherMatch,
3337
- controller: new AbortController(),
3392
+ let shouldRevalidate =
3393
+ cancelledFetcherLoads.includes(key) ||
3394
+ isPerformingInitialLoad ||
3395
+ shouldRevalidateLoader(fetcherMatch, {
3396
+ currentUrl,
3397
+ currentParams: state.matches[state.matches.length - 1].params,
3398
+ nextUrl,
3399
+ nextParams: matches[matches.length - 1].params,
3400
+ ...submission,
3401
+ actionResult,
3402
+ defaultShouldRevalidate: isRevalidationRequired,
3338
3403
  });
3339
- return;
3340
- }
3341
3404
 
3342
- // Revalidating fetchers are decoupled from the route matches since they
3343
- // hit a static href, so they _always_ check shouldRevalidate and the
3344
- // default is strictly if a revalidation is explicitly required (action
3345
- // submissions, useRevalidator, X-Remix-Revalidate).
3346
- let shouldRevalidate = shouldRevalidateLoader(fetcherMatch, {
3347
- currentUrl,
3348
- currentParams: state.matches[state.matches.length - 1].params,
3349
- nextUrl,
3350
- nextParams: matches[matches.length - 1].params,
3351
- ...submission,
3352
- actionResult,
3353
- // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3354
- defaultShouldRevalidate: isRevalidationRequired,
3355
- });
3356
3405
  if (shouldRevalidate) {
3357
3406
  revalidatingFetchers.push({
3358
3407
  key,
@@ -3503,9 +3552,11 @@ async function callLoaderOrAction(
3503
3552
  manifest: RouteManifest,
3504
3553
  mapRouteProperties: MapRoutePropertiesFunction,
3505
3554
  basename: string,
3506
- isStaticRequest: boolean = false,
3507
- isRouteRequest: boolean = false,
3508
- requestContext?: unknown
3555
+ opts: {
3556
+ isStaticRequest?: boolean;
3557
+ isRouteRequest?: boolean;
3558
+ requestContext?: unknown;
3559
+ } = {}
3509
3560
  ): Promise<DataResult> {
3510
3561
  let resultType;
3511
3562
  let result;
@@ -3518,7 +3569,11 @@ async function callLoaderOrAction(
3518
3569
  onReject = () => reject();
3519
3570
  request.signal.addEventListener("abort", onReject);
3520
3571
  return Promise.race([
3521
- handler({ request, params: match.params, context: requestContext }),
3572
+ handler({
3573
+ request,
3574
+ params: match.params,
3575
+ context: opts.requestContext,
3576
+ }),
3522
3577
  abortPromise,
3523
3578
  ]);
3524
3579
  };
@@ -3603,7 +3658,7 @@ async function callLoaderOrAction(
3603
3658
  true,
3604
3659
  location
3605
3660
  );
3606
- } else if (!isStaticRequest) {
3661
+ } else if (!opts.isStaticRequest) {
3607
3662
  // Strip off the protocol+origin for same-origin + same-basename absolute
3608
3663
  // redirects. If this is a static request, we can let it go back to the
3609
3664
  // browser as-is
@@ -3621,7 +3676,7 @@ async function callLoaderOrAction(
3621
3676
  // Instead, throw the Response and let the server handle it with an HTTP
3622
3677
  // redirect. We also update the Location header in place in this flow so
3623
3678
  // basename and relative routing is taken into account
3624
- if (isStaticRequest) {
3679
+ if (opts.isStaticRequest) {
3625
3680
  result.headers.set("Location", location);
3626
3681
  throw result;
3627
3682
  }
@@ -3637,7 +3692,7 @@ async function callLoaderOrAction(
3637
3692
  // For SSR single-route requests, we want to hand Responses back directly
3638
3693
  // without unwrapping. We do this with the QueryRouteResponse wrapper
3639
3694
  // interface so we can know whether it was returned or thrown
3640
- if (isRouteRequest) {
3695
+ if (opts.isRouteRequest) {
3641
3696
  // eslint-disable-next-line no-throw-literal
3642
3697
  throw {
3643
3698
  type: resultType || ResultType.data,
@@ -3700,18 +3755,30 @@ function createClientSideRequest(
3700
3755
  let init: RequestInit = { signal };
3701
3756
 
3702
3757
  if (submission && isMutationMethod(submission.formMethod)) {
3703
- let { formMethod, formEncType, formData } = submission;
3758
+ let { formMethod, formEncType } = submission;
3704
3759
  // Didn't think we needed this but it turns out unlike other methods, patch
3705
3760
  // won't be properly normalized to uppercase and results in a 405 error.
3706
3761
  // See: https://fetch.spec.whatwg.org/#concept-method
3707
3762
  init.method = formMethod.toUpperCase();
3708
- init.body =
3709
- formEncType === "application/x-www-form-urlencoded"
3710
- ? convertFormDataToSearchParams(formData)
3711
- : formData;
3763
+
3764
+ if (formEncType === "application/json") {
3765
+ init.headers = new Headers({ "Content-Type": formEncType });
3766
+ init.body = JSON.stringify(submission.json);
3767
+ } else if (formEncType === "text/plain") {
3768
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3769
+ init.body = submission.text;
3770
+ } else if (
3771
+ formEncType === "application/x-www-form-urlencoded" &&
3772
+ submission.formData
3773
+ ) {
3774
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3775
+ init.body = convertFormDataToSearchParams(submission.formData);
3776
+ } else {
3777
+ // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3778
+ init.body = submission.formData;
3779
+ }
3712
3780
  }
3713
3781
 
3714
- // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3715
3782
  return new Request(url, init);
3716
3783
  }
3717
3784
 
@@ -3720,12 +3787,22 @@ function convertFormDataToSearchParams(formData: FormData): URLSearchParams {
3720
3787
 
3721
3788
  for (let [key, value] of formData.entries()) {
3722
3789
  // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
3723
- searchParams.append(key, value instanceof File ? value.name : value);
3790
+ searchParams.append(key, typeof value === "string" ? value : value.name);
3724
3791
  }
3725
3792
 
3726
3793
  return searchParams;
3727
3794
  }
3728
3795
 
3796
+ function convertSearchParamsToFormData(
3797
+ searchParams: URLSearchParams
3798
+ ): FormData {
3799
+ let formData = new FormData();
3800
+ for (let [key, value] of searchParams.entries()) {
3801
+ formData.append(key, value);
3802
+ }
3803
+ return formData;
3804
+ }
3805
+
3729
3806
  function processRouteLoaderData(
3730
3807
  matches: AgnosticDataRouteMatch[],
3731
3808
  matchesToLoad: AgnosticDataRouteMatch[],
@@ -3877,15 +3954,7 @@ function processLoaderData(
3877
3954
  // in resolveDeferredResults
3878
3955
  invariant(false, "Unhandled fetcher deferred data");
3879
3956
  } else {
3880
- let doneFetcher: FetcherStates["Idle"] = {
3881
- state: "idle",
3882
- data: result.data,
3883
- formMethod: undefined,
3884
- formAction: undefined,
3885
- formEncType: undefined,
3886
- formData: undefined,
3887
- " _hasFetcherDoneAnything ": true,
3888
- };
3957
+ let doneFetcher = getDoneFetcher(result.data);
3889
3958
  state.fetchers.set(key, doneFetcher);
3890
3959
  }
3891
3960
  }
@@ -3973,7 +4042,7 @@ function getInternalRouterError(
3973
4042
  pathname?: string;
3974
4043
  routeId?: string;
3975
4044
  method?: string;
3976
- type?: "defer-action";
4045
+ type?: "defer-action" | "invalid-body";
3977
4046
  } = {}
3978
4047
  ) {
3979
4048
  let statusText = "Unknown Server Error";
@@ -3988,6 +4057,8 @@ function getInternalRouterError(
3988
4057
  `so there is no way to handle the request.`;
3989
4058
  } else if (type === "defer-action") {
3990
4059
  errorMessage = "defer() is not supported in actions";
4060
+ } else if (type === "invalid-body") {
4061
+ errorMessage = "Unable to encode submission body";
3991
4062
  }
3992
4063
  } else if (status === 403) {
3993
4064
  statusText = "Forbidden";
@@ -4226,4 +4297,157 @@ function getTargetMatch(
4226
4297
  let pathMatches = getPathContributingMatches(matches);
4227
4298
  return pathMatches[pathMatches.length - 1];
4228
4299
  }
4300
+
4301
+ function getSubmissionFromNavigation(
4302
+ navigation: Navigation
4303
+ ): Submission | undefined {
4304
+ let { formMethod, formAction, formEncType, text, formData, json } =
4305
+ navigation;
4306
+ if (!formMethod || !formAction || !formEncType) {
4307
+ return;
4308
+ }
4309
+
4310
+ if (text != null) {
4311
+ return {
4312
+ formMethod,
4313
+ formAction,
4314
+ formEncType,
4315
+ formData: undefined,
4316
+ json: undefined,
4317
+ text,
4318
+ };
4319
+ } else if (formData != null) {
4320
+ return {
4321
+ formMethod,
4322
+ formAction,
4323
+ formEncType,
4324
+ formData,
4325
+ json: undefined,
4326
+ text: undefined,
4327
+ };
4328
+ } else if (json !== undefined) {
4329
+ return {
4330
+ formMethod,
4331
+ formAction,
4332
+ formEncType,
4333
+ formData: undefined,
4334
+ json,
4335
+ text: undefined,
4336
+ };
4337
+ }
4338
+ }
4339
+
4340
+ function getLoadingNavigation(
4341
+ location: Location,
4342
+ submission?: Submission
4343
+ ): NavigationStates["Loading"] {
4344
+ if (submission) {
4345
+ let navigation: NavigationStates["Loading"] = {
4346
+ state: "loading",
4347
+ location,
4348
+ formMethod: submission.formMethod,
4349
+ formAction: submission.formAction,
4350
+ formEncType: submission.formEncType,
4351
+ formData: submission.formData,
4352
+ json: submission.json,
4353
+ text: submission.text,
4354
+ };
4355
+ return navigation;
4356
+ } else {
4357
+ let navigation: NavigationStates["Loading"] = {
4358
+ state: "loading",
4359
+ location,
4360
+ formMethod: undefined,
4361
+ formAction: undefined,
4362
+ formEncType: undefined,
4363
+ formData: undefined,
4364
+ json: undefined,
4365
+ text: undefined,
4366
+ };
4367
+ return navigation;
4368
+ }
4369
+ }
4370
+
4371
+ function getSubmittingNavigation(
4372
+ location: Location,
4373
+ submission: Submission
4374
+ ): NavigationStates["Submitting"] {
4375
+ let navigation: NavigationStates["Submitting"] = {
4376
+ state: "submitting",
4377
+ location,
4378
+ formMethod: submission.formMethod,
4379
+ formAction: submission.formAction,
4380
+ formEncType: submission.formEncType,
4381
+ formData: submission.formData,
4382
+ json: submission.json,
4383
+ text: submission.text,
4384
+ };
4385
+ return navigation;
4386
+ }
4387
+
4388
+ function getLoadingFetcher(
4389
+ submission?: Submission,
4390
+ data?: Fetcher["data"]
4391
+ ): FetcherStates["Loading"] {
4392
+ if (submission) {
4393
+ let fetcher: FetcherStates["Loading"] = {
4394
+ state: "loading",
4395
+ formMethod: submission.formMethod,
4396
+ formAction: submission.formAction,
4397
+ formEncType: submission.formEncType,
4398
+ formData: submission.formData,
4399
+ json: submission.json,
4400
+ text: submission.text,
4401
+ data,
4402
+ " _hasFetcherDoneAnything ": true,
4403
+ };
4404
+ return fetcher;
4405
+ } else {
4406
+ let fetcher: FetcherStates["Loading"] = {
4407
+ state: "loading",
4408
+ formMethod: undefined,
4409
+ formAction: undefined,
4410
+ formEncType: undefined,
4411
+ formData: undefined,
4412
+ json: undefined,
4413
+ text: undefined,
4414
+ data,
4415
+ " _hasFetcherDoneAnything ": true,
4416
+ };
4417
+ return fetcher;
4418
+ }
4419
+ }
4420
+
4421
+ function getSubmittingFetcher(
4422
+ submission: Submission,
4423
+ existingFetcher?: Fetcher
4424
+ ): FetcherStates["Submitting"] {
4425
+ let fetcher: FetcherStates["Submitting"] = {
4426
+ state: "submitting",
4427
+ formMethod: submission.formMethod,
4428
+ formAction: submission.formAction,
4429
+ formEncType: submission.formEncType,
4430
+ formData: submission.formData,
4431
+ json: submission.json,
4432
+ text: submission.text,
4433
+ data: existingFetcher ? existingFetcher.data : undefined,
4434
+ " _hasFetcherDoneAnything ": true,
4435
+ };
4436
+ return fetcher;
4437
+ }
4438
+
4439
+ function getDoneFetcher(data: Fetcher["data"]): FetcherStates["Idle"] {
4440
+ let fetcher: FetcherStates["Idle"] = {
4441
+ state: "idle",
4442
+ formMethod: undefined,
4443
+ formAction: undefined,
4444
+ formEncType: undefined,
4445
+ formData: undefined,
4446
+ json: undefined,
4447
+ text: undefined,
4448
+ data,
4449
+ " _hasFetcherDoneAnything ": true,
4450
+ };
4451
+ return fetcher;
4452
+ }
4229
4453
  //#endregion