@remix-run/router 1.1.0 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -435,6 +435,7 @@ type FetcherStates<TData = any> = {
435
435
  formEncType: undefined;
436
436
  formData: undefined;
437
437
  data: TData | undefined;
438
+ " _hasFetcherDoneAnything "?: boolean;
438
439
  };
439
440
  Loading: {
440
441
  state: "loading";
@@ -443,6 +444,7 @@ type FetcherStates<TData = any> = {
443
444
  formEncType: FormEncType | undefined;
444
445
  formData: FormData | undefined;
445
446
  data: TData | undefined;
447
+ " _hasFetcherDoneAnything "?: boolean;
446
448
  };
447
449
  Submitting: {
448
450
  state: "submitting";
@@ -451,6 +453,7 @@ type FetcherStates<TData = any> = {
451
453
  formEncType: FormEncType;
452
454
  formData: FormData;
453
455
  data: TData | undefined;
456
+ " _hasFetcherDoneAnything "?: boolean;
454
457
  };
455
458
  };
456
459
 
@@ -593,7 +596,9 @@ export function createRouter(init: RouterInit): Router {
593
596
  // we don't get the saved positions from <ScrollRestoration /> until _after_
594
597
  // the initial render, we need to manually trigger a separate updateState to
595
598
  // send along the restoreScrollPosition
596
- let initialScrollRestored = false;
599
+ // Set to true if we have `hydrationData` since we assume we were SSR'd and that
600
+ // SSR did the initial scroll restoration.
601
+ let initialScrollRestored = init.hydrationData != null;
597
602
 
598
603
  let initialMatches = matchRoutes(
599
604
  dataRoutes,
@@ -623,7 +628,8 @@ export function createRouter(init: RouterInit): Router {
623
628
  matches: initialMatches,
624
629
  initialized,
625
630
  navigation: IDLE_NAVIGATION,
626
- restoreScrollPosition: null,
631
+ // Don't restore on initial updateState() if we were SSR'd
632
+ restoreScrollPosition: init.hydrationData != null ? false : null,
627
633
  preventScrollReset: false,
628
634
  revalidation: "idle",
629
635
  loaderData: (init.hydrationData && init.hydrationData.loaderData) || {},
@@ -741,24 +747,36 @@ export function createRouter(init: RouterInit): Router {
741
747
  state.navigation.state === "loading" &&
742
748
  state.navigation.formAction?.split("?")[0] === location.pathname;
743
749
 
750
+ let actionData: RouteData | null;
751
+ if (newState.actionData) {
752
+ if (Object.keys(newState.actionData).length > 0) {
753
+ actionData = newState.actionData;
754
+ } else {
755
+ // Empty actionData -> clear prior actionData due to an action error
756
+ actionData = null;
757
+ }
758
+ } else if (isActionReload) {
759
+ // Keep the current data if we're wrapping up the action reload
760
+ actionData = state.actionData;
761
+ } else {
762
+ // Clear actionData on any other completed navigations
763
+ actionData = null;
764
+ }
765
+
744
766
  // Always preserve any existing loaderData from re-used routes
745
- let newLoaderData = newState.loaderData
746
- ? {
747
- loaderData: mergeLoaderData(
748
- state.loaderData,
749
- newState.loaderData,
750
- newState.matches || []
751
- ),
752
- }
753
- : {};
767
+ let loaderData = newState.loaderData
768
+ ? mergeLoaderData(
769
+ state.loaderData,
770
+ newState.loaderData,
771
+ newState.matches || [],
772
+ newState.errors
773
+ )
774
+ : state.loaderData;
754
775
 
755
776
  updateState({
756
- // Clear existing actionData on any completed navigation beyond the original
757
- // action, unless we're currently finishing the loading/actionReload state.
758
- // Do this prior to spreading in newState in case we got back to back actions
759
- ...(isActionReload ? {} : { actionData: null }),
760
- ...newState,
761
- ...newLoaderData,
777
+ ...newState, // matches, errors, fetchers go through as-is
778
+ actionData,
779
+ loaderData,
762
780
  historyAction: pendingAction,
763
781
  location,
764
782
  initialized: true,
@@ -815,11 +833,26 @@ export function createRouter(init: RouterInit): Router {
815
833
  ...init.history.encodeLocation(location),
816
834
  };
817
835
 
818
- let historyAction =
819
- (opts && opts.replace) === true ||
820
- (submission != null && isMutationMethod(submission.formMethod))
821
- ? HistoryAction.Replace
822
- : HistoryAction.Push;
836
+ let userReplace = opts && opts.replace != null ? opts.replace : undefined;
837
+
838
+ let historyAction = HistoryAction.Push;
839
+
840
+ if (userReplace === true) {
841
+ historyAction = HistoryAction.Replace;
842
+ } else if (userReplace === false) {
843
+ // no-op
844
+ } else if (
845
+ submission != null &&
846
+ isMutationMethod(submission.formMethod) &&
847
+ submission.formAction === state.location.pathname + state.location.search
848
+ ) {
849
+ // By default on submissions to the current location we REPLACE so that
850
+ // users don't have to double-click the back button to get to the prior
851
+ // location. If the user redirects to a different location from the
852
+ // action/loader this will be ignored and the redirect will be a PUSH
853
+ historyAction = HistoryAction.Replace;
854
+ }
855
+
823
856
  let preventScrollReset =
824
857
  opts && "preventScrollReset" in opts
825
858
  ? opts.preventScrollReset === true
@@ -996,6 +1029,7 @@ export function createRouter(init: RouterInit): Router {
996
1029
 
997
1030
  completeNavigation(location, {
998
1031
  matches,
1032
+ ...(pendingActionData ? { actionData: pendingActionData } : {}),
999
1033
  loaderData,
1000
1034
  errors,
1001
1035
  });
@@ -1048,11 +1082,17 @@ export function createRouter(init: RouterInit): Router {
1048
1082
  }
1049
1083
 
1050
1084
  if (isRedirectResult(result)) {
1051
- await startRedirectNavigation(
1052
- state,
1053
- result,
1054
- opts && opts.replace === true
1055
- );
1085
+ let replace: boolean;
1086
+ if (opts && opts.replace != null) {
1087
+ replace = opts.replace;
1088
+ } else {
1089
+ // If the user didn't explicity indicate replace behavior, replace if
1090
+ // we redirected to the exact same location we're currently at to avoid
1091
+ // double back-buttons
1092
+ replace =
1093
+ result.location === state.location.pathname + state.location.search;
1094
+ }
1095
+ await startRedirectNavigation(state, result, replace);
1056
1096
  return { shortCircuited: true };
1057
1097
  }
1058
1098
 
@@ -1070,6 +1110,8 @@ export function createRouter(init: RouterInit): Router {
1070
1110
  }
1071
1111
 
1072
1112
  return {
1113
+ // Send back an empty object we can use to clear out any prior actionData
1114
+ pendingActionData: {},
1073
1115
  pendingActionError: { [boundaryMatch.route.id]: result.error },
1074
1116
  };
1075
1117
  }
@@ -1136,10 +1178,10 @@ export function createRouter(init: RouterInit): Router {
1136
1178
  if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
1137
1179
  completeNavigation(location, {
1138
1180
  matches,
1139
- loaderData: mergeLoaderData(state.loaderData, {}, matches),
1181
+ loaderData: {},
1140
1182
  // Commit pending error if we're short circuiting
1141
1183
  errors: pendingError || null,
1142
- actionData: pendingActionData || null,
1184
+ ...(pendingActionData ? { actionData: pendingActionData } : {}),
1143
1185
  });
1144
1186
  return { shortCircuited: true };
1145
1187
  }
@@ -1158,12 +1200,18 @@ export function createRouter(init: RouterInit): Router {
1158
1200
  formAction: undefined,
1159
1201
  formEncType: undefined,
1160
1202
  formData: undefined,
1203
+ " _hasFetcherDoneAnything ": true,
1161
1204
  };
1162
1205
  state.fetchers.set(key, revalidatingFetcher);
1163
1206
  });
1207
+ let actionData = pendingActionData || state.actionData;
1164
1208
  updateState({
1165
1209
  navigation: loadingNavigation,
1166
- actionData: pendingActionData || state.actionData || null,
1210
+ ...(actionData
1211
+ ? Object.keys(actionData).length === 0
1212
+ ? { actionData: null }
1213
+ : { actionData }
1214
+ : {}),
1167
1215
  ...(revalidatingFetchers.length > 0
1168
1216
  ? { fetchers: new Map(state.fetchers) }
1169
1217
  : {}),
@@ -1310,6 +1358,7 @@ export function createRouter(init: RouterInit): Router {
1310
1358
  state: "submitting",
1311
1359
  ...submission,
1312
1360
  data: existingFetcher && existingFetcher.data,
1361
+ " _hasFetcherDoneAnything ": true,
1313
1362
  };
1314
1363
  state.fetchers.set(key, fetcher);
1315
1364
  updateState({ fetchers: new Map(state.fetchers) });
@@ -1347,11 +1396,12 @@ export function createRouter(init: RouterInit): Router {
1347
1396
  state: "loading",
1348
1397
  ...submission,
1349
1398
  data: undefined,
1399
+ " _hasFetcherDoneAnything ": true,
1350
1400
  };
1351
1401
  state.fetchers.set(key, loadingFetcher);
1352
1402
  updateState({ fetchers: new Map(state.fetchers) });
1353
1403
 
1354
- return startRedirectNavigation(state, actionResult);
1404
+ return startRedirectNavigation(state, actionResult, false, true);
1355
1405
  }
1356
1406
 
1357
1407
  // Process any non-redirect errors thrown
@@ -1385,6 +1435,7 @@ export function createRouter(init: RouterInit): Router {
1385
1435
  state: "loading",
1386
1436
  data: actionResult.data,
1387
1437
  ...submission,
1438
+ " _hasFetcherDoneAnything ": true,
1388
1439
  };
1389
1440
  state.fetchers.set(key, loadFetcher);
1390
1441
 
@@ -1415,6 +1466,7 @@ export function createRouter(init: RouterInit): Router {
1415
1466
  formAction: undefined,
1416
1467
  formEncType: undefined,
1417
1468
  formData: undefined,
1469
+ " _hasFetcherDoneAnything ": true,
1418
1470
  };
1419
1471
  state.fetchers.set(staleKey, revalidatingFetcher);
1420
1472
  fetchControllers.set(staleKey, abortController);
@@ -1465,6 +1517,7 @@ export function createRouter(init: RouterInit): Router {
1465
1517
  formAction: undefined,
1466
1518
  formEncType: undefined,
1467
1519
  formData: undefined,
1520
+ " _hasFetcherDoneAnything ": true,
1468
1521
  };
1469
1522
  state.fetchers.set(key, doneFetcher);
1470
1523
 
@@ -1492,7 +1545,12 @@ export function createRouter(init: RouterInit): Router {
1492
1545
  // manually merge here since we aren't going through completeNavigation
1493
1546
  updateState({
1494
1547
  errors,
1495
- loaderData: mergeLoaderData(state.loaderData, loaderData, matches),
1548
+ loaderData: mergeLoaderData(
1549
+ state.loaderData,
1550
+ loaderData,
1551
+ matches,
1552
+ errors
1553
+ ),
1496
1554
  ...(didAbortFetchLoads ? { fetchers: new Map(state.fetchers) } : {}),
1497
1555
  });
1498
1556
  isRevalidationRequired = false;
@@ -1518,6 +1576,7 @@ export function createRouter(init: RouterInit): Router {
1518
1576
  formData: undefined,
1519
1577
  ...submission,
1520
1578
  data: existingFetcher && existingFetcher.data,
1579
+ " _hasFetcherDoneAnything ": true,
1521
1580
  };
1522
1581
  state.fetchers.set(key, loadingFetcher);
1523
1582
  updateState({ fetchers: new Map(state.fetchers) });
@@ -1586,6 +1645,7 @@ export function createRouter(init: RouterInit): Router {
1586
1645
  formAction: undefined,
1587
1646
  formEncType: undefined,
1588
1647
  formData: undefined,
1648
+ " _hasFetcherDoneAnything ": true,
1589
1649
  };
1590
1650
  state.fetchers.set(key, doneFetcher);
1591
1651
  updateState({ fetchers: new Map(state.fetchers) });
@@ -1613,13 +1673,22 @@ export function createRouter(init: RouterInit): Router {
1613
1673
  async function startRedirectNavigation(
1614
1674
  state: RouterState,
1615
1675
  redirect: RedirectResult,
1616
- replace?: boolean
1676
+ replace?: boolean,
1677
+ isFetchActionRedirect?: boolean
1617
1678
  ) {
1618
1679
  if (redirect.revalidate) {
1619
1680
  isRevalidationRequired = true;
1620
1681
  }
1621
1682
 
1622
- let redirectLocation = createLocation(state.location, redirect.location);
1683
+ let redirectLocation = createLocation(
1684
+ state.location,
1685
+ redirect.location,
1686
+ // TODO: This can be removed once we get rid of useTransition in Remix v2
1687
+ {
1688
+ _isRedirect: true,
1689
+ ...(isFetchActionRedirect ? { _isFetchActionRedirect: true } : {}),
1690
+ }
1691
+ );
1623
1692
  invariant(
1624
1693
  redirectLocation,
1625
1694
  "Expected a location on the redirect navigation"
@@ -1782,6 +1851,7 @@ export function createRouter(init: RouterInit): Router {
1782
1851
  formAction: undefined,
1783
1852
  formEncType: undefined,
1784
1853
  formData: undefined,
1854
+ " _hasFetcherDoneAnything ": true,
1785
1855
  };
1786
1856
  state.fetchers.set(key, doneFetcher);
1787
1857
  }
@@ -1928,7 +1998,7 @@ export function createRouter(init: RouterInit): Router {
1928
1998
  //#region createStaticHandler
1929
1999
  ////////////////////////////////////////////////////////////////////////////////
1930
2000
 
1931
- export function unstable_createStaticHandler(
2001
+ export function createStaticHandler(
1932
2002
  routes: AgnosticRouteObject[],
1933
2003
  opts?: {
1934
2004
  basename?: string;
@@ -1936,7 +2006,7 @@ export function unstable_createStaticHandler(
1936
2006
  ): StaticHandler {
1937
2007
  invariant(
1938
2008
  routes.length > 0,
1939
- "You must provide a non-empty routes array to unstable_createStaticHandler"
2009
+ "You must provide a non-empty routes array to createStaticHandler"
1940
2010
  );
1941
2011
 
1942
2012
  let dataRoutes = convertRoutesToDataRoutes(routes);
@@ -2313,7 +2383,11 @@ export function unstable_createStaticHandler(
2313
2383
  if (matchesToLoad.length === 0) {
2314
2384
  return {
2315
2385
  matches,
2316
- loaderData: {},
2386
+ // Add a null for all matched routes for proper revalidation on the client
2387
+ loaderData: matches.reduce(
2388
+ (acc, m) => Object.assign(acc, { [m.route.id]: null }),
2389
+ {}
2390
+ ),
2317
2391
  errors: pendingActionError || null,
2318
2392
  statusCode: 200,
2319
2393
  loaderHeaders: {},
@@ -2340,9 +2414,11 @@ export function unstable_createStaticHandler(
2340
2414
  throw new Error(`${method}() call aborted`);
2341
2415
  }
2342
2416
 
2343
- // Can't do anything with these without the Remix side of things, so just
2344
- // cancel them for now
2345
- results.forEach((result) => {
2417
+ let executedLoaders = new Set<string>();
2418
+ results.forEach((result, i) => {
2419
+ executedLoaders.add(matchesToLoad[i].route.id);
2420
+ // Can't do anything with these without the Remix side of things, so just
2421
+ // cancel them for now
2346
2422
  if (isDeferredResult(result)) {
2347
2423
  result.deferredData.cancel();
2348
2424
  }
@@ -2356,6 +2432,13 @@ export function unstable_createStaticHandler(
2356
2432
  pendingActionError
2357
2433
  );
2358
2434
 
2435
+ // Add a null for any non-loader matches for proper revalidation on the client
2436
+ matches.forEach((match) => {
2437
+ if (!executedLoaders.has(match.route.id)) {
2438
+ context.loaderData[match.route.id] = null;
2439
+ }
2440
+ });
2441
+
2359
2442
  return {
2360
2443
  ...context,
2361
2444
  matches,
@@ -2498,7 +2581,7 @@ function getMatchesToLoad(
2498
2581
  ? Object.values(pendingError)[0]
2499
2582
  : pendingActionData
2500
2583
  ? Object.values(pendingActionData)[0]
2501
- : null;
2584
+ : undefined;
2502
2585
 
2503
2586
  // Pick navigation matches that are net-new or qualify for revalidation
2504
2587
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
@@ -2742,7 +2825,9 @@ async function callLoaderOrAction(
2742
2825
 
2743
2826
  let data: any;
2744
2827
  let contentType = result.headers.get("Content-Type");
2745
- if (contentType && contentType.startsWith("application/json")) {
2828
+ // Check between word boundaries instead of startsWith() due to the last
2829
+ // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
2830
+ if (contentType && /\bapplication\/json\b/.test(contentType)) {
2746
2831
  data = await result.json();
2747
2832
  } else {
2748
2833
  data = await result.text();
@@ -2860,6 +2945,9 @@ function processRouteLoaderData(
2860
2945
  errors[boundaryMatch.route.id] = error;
2861
2946
  }
2862
2947
 
2948
+ // Clear our any prior loaderData for the throwing route
2949
+ loaderData[id] = undefined;
2950
+
2863
2951
  // Once we find our first (highest) error, we set the status code and
2864
2952
  // prevent deeper status codes from overriding
2865
2953
  if (!foundError) {
@@ -2893,9 +2981,11 @@ function processRouteLoaderData(
2893
2981
  });
2894
2982
 
2895
2983
  // If we didn't consume the pending action error (i.e., all loaders
2896
- // resolved), then consume it here
2984
+ // resolved), then consume it here. Also clear out any loaderData for the
2985
+ // throwing route
2897
2986
  if (pendingError) {
2898
2987
  errors = pendingError;
2988
+ loaderData[Object.keys(pendingError)[0]] = undefined;
2899
2989
  }
2900
2990
 
2901
2991
  return {
@@ -2962,6 +3052,7 @@ function processLoaderData(
2962
3052
  formAction: undefined,
2963
3053
  formEncType: undefined,
2964
3054
  formData: undefined,
3055
+ " _hasFetcherDoneAnything ": true,
2965
3056
  };
2966
3057
  state.fetchers.set(key, doneFetcher);
2967
3058
  }
@@ -2973,15 +3064,29 @@ function processLoaderData(
2973
3064
  function mergeLoaderData(
2974
3065
  loaderData: RouteData,
2975
3066
  newLoaderData: RouteData,
2976
- matches: AgnosticDataRouteMatch[]
3067
+ matches: AgnosticDataRouteMatch[],
3068
+ errors: RouteData | null | undefined
2977
3069
  ): RouteData {
2978
3070
  let mergedLoaderData = { ...newLoaderData };
2979
- matches.forEach((match) => {
3071
+ for (let match of matches) {
2980
3072
  let id = match.route.id;
2981
- if (newLoaderData[id] === undefined && loaderData[id] !== undefined) {
3073
+ if (newLoaderData.hasOwnProperty(id)) {
3074
+ if (newLoaderData[id] !== undefined) {
3075
+ mergedLoaderData[id] = newLoaderData[id];
3076
+ } else {
3077
+ // No-op - this is so we ignore existing data if we have a key in the
3078
+ // incoming object with an undefined value, which is how we unset a prior
3079
+ // loaderData if we encounter a loader error
3080
+ }
3081
+ } else if (loaderData[id] !== undefined) {
2982
3082
  mergedLoaderData[id] = loaderData[id];
2983
3083
  }
2984
- });
3084
+
3085
+ if (errors && errors.hasOwnProperty(id)) {
3086
+ // Don't keep any loader data below the boundary
3087
+ break;
3088
+ }
3089
+ }
2985
3090
  return mergedLoaderData;
2986
3091
  }
2987
3092