@remix-run/router 1.10.0 → 1.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.10.0",
3
+ "version": "1.11.0-pre.1",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -193,7 +193,7 @@ export interface Router {
193
193
  * Get/create a fetcher for the given key
194
194
  * @param key
195
195
  */
196
- getFetcher<TData = any>(key?: string): Fetcher<TData>;
196
+ getFetcher<TData = any>(key: string): Fetcher<TData>;
197
197
 
198
198
  /**
199
199
  * @internal
@@ -202,7 +202,7 @@ export interface Router {
202
202
  * Delete the fetcher for a given key
203
203
  * @param key
204
204
  */
205
- deleteFetcher(key?: string): void;
205
+ deleteFetcher(key: string): void;
206
206
 
207
207
  /**
208
208
  * @internal
@@ -343,6 +343,7 @@ export type HydrationState = Partial<
343
343
  * Future flags to toggle new feature behavior
344
344
  */
345
345
  export interface FutureConfig {
346
+ v7_fetcherPersist: boolean;
346
347
  v7_normalizeFormMethod: boolean;
347
348
  v7_prependBasename: boolean;
348
349
  }
@@ -408,6 +409,7 @@ export interface RouterSubscriber {
408
409
  (
409
410
  state: RouterState,
410
411
  opts: {
412
+ deletedFetchers: string[];
411
413
  unstable_viewTransitionOpts?: ViewTransitionOpts;
412
414
  }
413
415
  ): void;
@@ -763,6 +765,7 @@ export function createRouter(init: RouterInit): Router {
763
765
  let basename = init.basename || "/";
764
766
  // Config driven behavior flags
765
767
  let future: FutureConfig = {
768
+ v7_fetcherPersist: false,
766
769
  v7_normalizeFormMethod: false,
767
770
  v7_prependBasename: false,
768
771
  ...init.future,
@@ -885,6 +888,13 @@ export function createRouter(init: RouterInit): Router {
885
888
  // Most recent href/match for fetcher.load calls for fetchers
886
889
  let fetchLoadMatches = new Map<string, FetchLoadMatch>();
887
890
 
891
+ // Ref-count mounted fetchers so we know when it's ok to clean them up
892
+ let activeFetchers = new Map<string, number>();
893
+
894
+ // Fetchers that have requested a delete when using v7_fetcherPersist,
895
+ // they'll be officially removed after they return to idle
896
+ let deletedFetchers = new Set<string>();
897
+
888
898
  // Store DeferredData instances for active route matches. When a
889
899
  // route loader returns defer() we stick one in here. Then, when a nested
890
900
  // promise resolves we update loaderData. If a new navigation starts we
@@ -1014,9 +1024,39 @@ export function createRouter(init: RouterInit): Router {
1014
1024
  ...state,
1015
1025
  ...newState,
1016
1026
  };
1027
+
1028
+ // Prep fetcher cleanup so we can tell the UI which fetcher data entries
1029
+ // can be removed
1030
+ let completedFetchers: string[] = [];
1031
+ let deletedFetchersKeys: string[] = [];
1032
+
1033
+ if (future.v7_fetcherPersist) {
1034
+ state.fetchers.forEach((fetcher, key) => {
1035
+ if (fetcher.state === "idle") {
1036
+ if (deletedFetchers.has(key)) {
1037
+ // Unmounted from the UI and can be totally removed
1038
+ deletedFetchersKeys.push(key);
1039
+ } else {
1040
+ // Returned to idle but still mounted in the UI, so semi-remains for
1041
+ // revalidations and such
1042
+ completedFetchers.push(key);
1043
+ }
1044
+ }
1045
+ });
1046
+ }
1047
+
1017
1048
  subscribers.forEach((subscriber) =>
1018
- subscriber(state, { unstable_viewTransitionOpts: viewTransitionOpts })
1049
+ subscriber(state, {
1050
+ deletedFetchers: deletedFetchersKeys,
1051
+ unstable_viewTransitionOpts: viewTransitionOpts,
1052
+ })
1019
1053
  );
1054
+
1055
+ // Remove idle fetchers from state since we only care about in-flight fetchers.
1056
+ if (future.v7_fetcherPersist) {
1057
+ completedFetchers.forEach((key) => state.fetchers.delete(key));
1058
+ deletedFetchersKeys.forEach((key) => deleteFetcher(key));
1059
+ }
1020
1060
  }
1021
1061
 
1022
1062
  // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
@@ -1725,6 +1765,14 @@ export function createRouter(init: RouterInit): Router {
1725
1765
  }
1726
1766
 
1727
1767
  function getFetcher<TData = any>(key: string): Fetcher<TData> {
1768
+ if (future.v7_fetcherPersist) {
1769
+ activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
1770
+ // If this fetcher was previously marked for deletion, unmark it since we
1771
+ // have a new instance
1772
+ if (deletedFetchers.has(key)) {
1773
+ deletedFetchers.delete(key);
1774
+ }
1775
+ }
1728
1776
  return state.fetchers.get(key) || IDLE_FETCHER;
1729
1777
  }
1730
1778
 
@@ -1844,7 +1892,7 @@ export function createRouter(init: RouterInit): Router {
1844
1892
  );
1845
1893
 
1846
1894
  if (fetchRequest.signal.aborted) {
1847
- // We can delete this so long as we weren't aborted by ou our own fetcher
1895
+ // We can delete this so long as we weren't aborted by our own fetcher
1848
1896
  // re-submit which would have put _new_ controller is in fetchControllers
1849
1897
  if (fetchControllers.get(key) === abortController) {
1850
1898
  fetchControllers.delete(key);
@@ -1852,6 +1900,12 @@ export function createRouter(init: RouterInit): Router {
1852
1900
  return;
1853
1901
  }
1854
1902
 
1903
+ if (deletedFetchers.has(key)) {
1904
+ state.fetchers.set(key, getDoneFetcher(undefined));
1905
+ updateState({ fetchers: new Map(state.fetchers) });
1906
+ return;
1907
+ }
1908
+
1855
1909
  if (isRedirectResult(actionResult)) {
1856
1910
  fetchControllers.delete(key);
1857
1911
  if (pendingNavigationLoadId > originatingLoadId) {
@@ -2009,7 +2063,7 @@ export function createRouter(init: RouterInit): Router {
2009
2063
  state.fetchers.set(key, doneFetcher);
2010
2064
  }
2011
2065
 
2012
- let didAbortFetchLoads = abortStaleFetchLoads(loadId);
2066
+ abortStaleFetchLoads(loadId);
2013
2067
 
2014
2068
  // If we are currently in a navigation loading state and this fetcher is
2015
2069
  // more recent than the navigation, we want the newer data so abort the
@@ -2039,9 +2093,7 @@ export function createRouter(init: RouterInit): Router {
2039
2093
  matches,
2040
2094
  errors
2041
2095
  ),
2042
- ...(didAbortFetchLoads || revalidatingFetchers.length > 0
2043
- ? { fetchers: new Map(state.fetchers) }
2044
- : {}),
2096
+ fetchers: new Map(state.fetchers),
2045
2097
  });
2046
2098
  isRevalidationRequired = false;
2047
2099
  }
@@ -2105,6 +2157,12 @@ export function createRouter(init: RouterInit): Router {
2105
2157
  return;
2106
2158
  }
2107
2159
 
2160
+ if (deletedFetchers.has(key)) {
2161
+ state.fetchers.set(key, getDoneFetcher(undefined));
2162
+ updateState({ fetchers: new Map(state.fetchers) });
2163
+ return;
2164
+ }
2165
+
2108
2166
  // If the loader threw a redirect Response, start a new REPLACE navigation
2109
2167
  if (isRedirectResult(result)) {
2110
2168
  if (pendingNavigationLoadId > originatingLoadId) {
@@ -2123,17 +2181,7 @@ export function createRouter(init: RouterInit): Router {
2123
2181
 
2124
2182
  // Process any non-redirect errors thrown
2125
2183
  if (isErrorResult(result)) {
2126
- let boundaryMatch = findNearestBoundary(state.matches, routeId);
2127
- state.fetchers.delete(key);
2128
- // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
2129
- // do we need to behave any differently with our non-redirect errors?
2130
- // What if it was a non-redirect Response?
2131
- updateState({
2132
- fetchers: new Map(state.fetchers),
2133
- errors: {
2134
- [boundaryMatch.route.id]: result.error,
2135
- },
2136
- });
2184
+ setFetcherError(key, routeId, result.error);
2137
2185
  return;
2138
2186
  }
2139
2187
 
@@ -2376,9 +2424,25 @@ export function createRouter(init: RouterInit): Router {
2376
2424
  fetchLoadMatches.delete(key);
2377
2425
  fetchReloadIds.delete(key);
2378
2426
  fetchRedirectIds.delete(key);
2427
+ deletedFetchers.delete(key);
2379
2428
  state.fetchers.delete(key);
2380
2429
  }
2381
2430
 
2431
+ function deleteFetcherAndUpdateState(key: string): void {
2432
+ if (future.v7_fetcherPersist) {
2433
+ let count = (activeFetchers.get(key) || 0) - 1;
2434
+ if (count <= 0) {
2435
+ activeFetchers.delete(key);
2436
+ deletedFetchers.add(key);
2437
+ } else {
2438
+ activeFetchers.set(key, count);
2439
+ }
2440
+ } else {
2441
+ deleteFetcher(key);
2442
+ }
2443
+ updateState({ fetchers: new Map(state.fetchers) });
2444
+ }
2445
+
2382
2446
  function abortFetcher(key: string) {
2383
2447
  let controller = fetchControllers.get(key);
2384
2448
  invariant(controller, `Expected fetch controller: ${key}`);
@@ -2613,7 +2677,7 @@ export function createRouter(init: RouterInit): Router {
2613
2677
  createHref: (to: To) => init.history.createHref(to),
2614
2678
  encodeLocation: (to: To) => init.history.encodeLocation(to),
2615
2679
  getFetcher,
2616
- deleteFetcher,
2680
+ deleteFetcher: deleteFetcherAndUpdateState,
2617
2681
  dispose,
2618
2682
  getBlocker,
2619
2683
  deleteBlocker,
package/utils.ts CHANGED
@@ -903,7 +903,7 @@ export function matchPath<
903
903
  pattern = { path: pattern, caseSensitive: false, end: true };
904
904
  }
905
905
 
906
- let [matcher, paramNames] = compilePath(
906
+ let [matcher, compiledParams] = compilePath(
907
907
  pattern.path,
908
908
  pattern.caseSensitive,
909
909
  pattern.end
@@ -915,8 +915,8 @@ export function matchPath<
915
915
  let matchedPathname = match[0];
916
916
  let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
917
917
  let captureGroups = match.slice(1);
918
- let params: Params = paramNames.reduce<Mutable<Params>>(
919
- (memo, paramName, index) => {
918
+ let params: Params = compiledParams.reduce<Mutable<Params>>(
919
+ (memo, { paramName, isOptional }, index) => {
920
920
  // We need to compute the pathnameBase here using the raw splat value
921
921
  // instead of using params["*"] later because it will be decoded then
922
922
  if (paramName === "*") {
@@ -926,10 +926,12 @@ export function matchPath<
926
926
  .replace(/(.)\/+$/, "$1");
927
927
  }
928
928
 
929
- memo[paramName] = safelyDecodeURIComponent(
930
- captureGroups[index] || "",
931
- paramName
932
- );
929
+ const value = captureGroups[index];
930
+ if (isOptional && !value) {
931
+ memo[paramName] = undefined;
932
+ } else {
933
+ memo[paramName] = safelyDecodeURIComponent(value || "", paramName);
934
+ }
933
935
  return memo;
934
936
  },
935
937
  {}
@@ -943,11 +945,13 @@ export function matchPath<
943
945
  };
944
946
  }
945
947
 
948
+ type CompiledPathParam = { paramName: string; isOptional?: boolean };
949
+
946
950
  function compilePath(
947
951
  path: string,
948
952
  caseSensitive = false,
949
953
  end = true
950
- ): [RegExp, string[]] {
954
+ ): [RegExp, CompiledPathParam[]] {
951
955
  warning(
952
956
  path === "*" || !path.endsWith("*") || path.endsWith("/*"),
953
957
  `Route path "${path}" will be treated as if it were ` +
@@ -956,20 +960,20 @@ function compilePath(
956
960
  `please change the route path to "${path.replace(/\*$/, "/*")}".`
957
961
  );
958
962
 
959
- let paramNames: string[] = [];
963
+ let params: CompiledPathParam[] = [];
960
964
  let regexpSource =
961
965
  "^" +
962
966
  path
963
967
  .replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
964
968
  .replace(/^\/*/, "/") // Make sure it has a leading /
965
- .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
966
- .replace(/\/:(\w+)/g, (_: string, paramName: string) => {
967
- paramNames.push(paramName);
968
- return "/([^\\/]+)";
969
+ .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
970
+ .replace(/\/:(\w+)(\?)?/g, (_: string, paramName: string, isOptional) => {
971
+ params.push({ paramName, isOptional: isOptional != null });
972
+ return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
969
973
  });
970
974
 
971
975
  if (path.endsWith("*")) {
972
- paramNames.push("*");
976
+ params.push({ paramName: "*" });
973
977
  regexpSource +=
974
978
  path === "*" || path === "/*"
975
979
  ? "(.*)$" // Already matched the initial /, just match the rest
@@ -992,7 +996,7 @@ function compilePath(
992
996
 
993
997
  let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
994
998
 
995
- return [matcher, paramNames];
999
+ return [matcher, params];
996
1000
  }
997
1001
 
998
1002
  function safelyDecodeURI(value: string) {