@remix-run/router 1.10.0-pre.0 → 1.11.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,49 +1,28 @@
1
1
  # `@remix-run/router`
2
2
 
3
- ## 1.10.0-pre.0
3
+ ## 1.11.0-pre.0
4
4
 
5
5
  ### Minor Changes
6
6
 
7
- - Add support for the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) via `document.startViewTransition` to enable CSS animated transitions on SPA navigations in your application. ([#10916](https://github.com/remix-run/react-router/pull/10916))
7
+ - Add a new `future.v7_fetcherPersist` flag to the `@remix-run/router` to change the persistence behavior of fetchers when `router.deleteFetcher` is called. Instead of being immediately cleaned up, fetchers will persist until they return to an `idle` state([RFC](https://github.com/remix-run/remix/discussions/7698)) ([#10962](https://github.com/remix-run/react-router/pull/10962))
8
8
 
9
- The simplest approach to enabling a View Transition in your React Router app is via the new `<Link unstable_viewTransition>` prop. This will cause the navigation DOM update to be wrapped in `document.startViewTransition` which will enable transitions for the DOM update. Without any additional CSS styles, you'll get a basic cross-fade animation for your page.
9
+ - This is sort of a long-standing bug fix as the `useFetchers()` API was always supposed to only reflect **in-flight** fetcher information for pending/optimistic UI -- it was not intended to reflect fetcher data or hang onto fetchers after they returned to an `idle` state
10
+ - With `v7_fetcherPersist`, the `router` only knows about in-flight fetchers - they do not exist in `state.fetchers` until a `fetch()` call is made, and they are removed as soon as it returns to `idle` (and the data is handed off to the React layer)
11
+ - Keep an eye out for the following specific behavioral changes when opting into this flag and check your app for compatibility:
12
+ - Fetchers that complete _while still mounted_ will no longer appear in `useFetchers()`. They served effectively no purpose in there since you can access the data via `useFetcher().data`).
13
+ - Fetchers that previously unmounted _while in-flight_ will not be immediately aborted and will instead be cleaned up once they return to an `idle` state. They will remain exposed via `useFetchers` while in-flight so you can still access pending/optimistic data after unmount.
10
14
 
11
- If you need to apply more fine-grained styles for your animations, you can leverage the `unstable_useViewTransitionState` hook which will tell you when a transition is in progress and you can use that to apply classes or styles:
15
+ - Add support for optional path segments in `matchPath` ([#10768](https://github.com/remix-run/react-router/pull/10768))
12
16
 
13
- ```jsx
14
- function ImageLink(to, src, alt) {
15
- let isTransitioning = unstable_useViewTransitionState(to);
16
- return (
17
- <Link to={to} unstable_viewTransition>
18
- <img
19
- src={src}
20
- alt={alt}
21
- style={{
22
- viewTransitionName: isTransitioning ? "image-expand" : "",
23
- }}
24
- />
25
- </Link>
26
- );
27
- }
28
- ```
29
-
30
- You can also use the `<NavLink unstable_viewTransition>` shorthand which will manage the hook usage for you and automatically add a `transitioning` class to the `<a>` during the transition:
17
+ ### Patch Changes
31
18
 
32
- ```css
33
- a.transitioning img {
34
- view-transition-name: "image-expand";
35
- }
36
- ```
19
+ - Fix `router.getFetcher`/`router.deleteFetcher` type definitions which incorrectly specified `key` as an optional parameter ([#10960](https://github.com/remix-run/react-router/pull/10960))
37
20
 
38
- ```jsx
39
- <NavLink to={to} unstable_viewTransition>
40
- <img src={src} alt={alt} />
41
- </NavLink>
42
- ```
21
+ ## 1.10.0
43
22
 
44
- For an example usage of View Transitions with React Router, check out [our fork](https://github.com/brophdawg11/react-router-records) of the [Astro Records](https://github.com/Charca/astro-records) demo.
23
+ ### Minor Changes
45
24
 
46
- For more information on using the View Transitions API, please refer to the [Smooth and simple transitions with the View Transitions API](https://developer.chrome.com/docs/web-platform/view-transitions/) guide from the Google Chrome team.
25
+ - Add experimental support for the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) by allowing users to opt-into view transitions on navigations via the new `unstable_viewTransition` option to `router.navigate` ([#10916](https://github.com/remix-run/react-router/pull/10916))
47
26
 
48
27
  ### Patch Changes
49
28
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.10.0-pre.0
2
+ * @remix-run/router v1.11.0-pre.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1017,20 +1017,29 @@ function matchPath(pattern, pathname) {
1017
1017
  end: true
1018
1018
  };
1019
1019
  }
1020
- let [matcher, paramNames] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
1020
+ let [matcher, compiledParams] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
1021
1021
  let match = pathname.match(matcher);
1022
1022
  if (!match) return null;
1023
1023
  let matchedPathname = match[0];
1024
1024
  let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
1025
1025
  let captureGroups = match.slice(1);
1026
- let params = paramNames.reduce((memo, paramName, index) => {
1026
+ let params = compiledParams.reduce((memo, _ref, index) => {
1027
+ let {
1028
+ paramName,
1029
+ isOptional
1030
+ } = _ref;
1027
1031
  // We need to compute the pathnameBase here using the raw splat value
1028
1032
  // instead of using params["*"] later because it will be decoded then
1029
1033
  if (paramName === "*") {
1030
1034
  let splatValue = captureGroups[index] || "";
1031
1035
  pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
1032
1036
  }
1033
- memo[paramName] = safelyDecodeURIComponent(captureGroups[index] || "", paramName);
1037
+ const value = captureGroups[index];
1038
+ if (isOptional && !value) {
1039
+ memo[paramName] = undefined;
1040
+ } else {
1041
+ memo[paramName] = safelyDecodeURIComponent(value || "", paramName);
1042
+ }
1034
1043
  return memo;
1035
1044
  }, {});
1036
1045
  return {
@@ -1048,16 +1057,21 @@ function compilePath(path, caseSensitive, end) {
1048
1057
  end = true;
1049
1058
  }
1050
1059
  warning(path === "*" || !path.endsWith("*") || path.endsWith("/*"), "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
1051
- let paramNames = [];
1060
+ let params = [];
1052
1061
  let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
1053
1062
  .replace(/^\/*/, "/") // Make sure it has a leading /
1054
- .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
1055
- .replace(/\/:(\w+)/g, (_, paramName) => {
1056
- paramNames.push(paramName);
1057
- return "/([^\\/]+)";
1063
+ .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
1064
+ .replace(/\/:(\w+)(\?)?/g, (_, paramName, isOptional) => {
1065
+ params.push({
1066
+ paramName,
1067
+ isOptional: isOptional != null
1068
+ });
1069
+ return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
1058
1070
  });
1059
1071
  if (path.endsWith("*")) {
1060
- paramNames.push("*");
1072
+ params.push({
1073
+ paramName: "*"
1074
+ });
1061
1075
  regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest
1062
1076
  : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
1063
1077
  } else if (end) {
@@ -1074,7 +1088,7 @@ function compilePath(path, caseSensitive, end) {
1074
1088
  regexpSource += "(?:(?=\\/|$))";
1075
1089
  } else ;
1076
1090
  let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
1077
- return [matcher, paramNames];
1091
+ return [matcher, params];
1078
1092
  }
1079
1093
  function safelyDecodeURI(value) {
1080
1094
  try {
@@ -1302,8 +1316,8 @@ class DeferredData {
1302
1316
  let onAbort = () => reject(new AbortedDeferredError("Deferred data aborted"));
1303
1317
  this.unlistenAbortSignal = () => this.controller.signal.removeEventListener("abort", onAbort);
1304
1318
  this.controller.signal.addEventListener("abort", onAbort);
1305
- this.data = Object.entries(data).reduce((acc, _ref) => {
1306
- let [key, value] = _ref;
1319
+ this.data = Object.entries(data).reduce((acc, _ref2) => {
1320
+ let [key, value] = _ref2;
1307
1321
  return Object.assign(acc, {
1308
1322
  [key]: this.trackPromise(key, value)
1309
1323
  });
@@ -1403,8 +1417,8 @@ class DeferredData {
1403
1417
  }
1404
1418
  get unwrappedData() {
1405
1419
  invariant(this.data !== null && this.done, "Can only unwrap data on initialized and settled deferreds");
1406
- return Object.entries(this.data).reduce((acc, _ref2) => {
1407
- let [key, value] = _ref2;
1420
+ return Object.entries(this.data).reduce((acc, _ref3) => {
1421
+ let [key, value] = _ref3;
1408
1422
  return Object.assign(acc, {
1409
1423
  [key]: unwrapTrackedPromise(value)
1410
1424
  });
@@ -1649,6 +1663,7 @@ function createRouter(init) {
1649
1663
  let basename = init.basename || "/";
1650
1664
  // Config driven behavior flags
1651
1665
  let future = _extends({
1666
+ v7_fetcherPersist: false,
1652
1667
  v7_normalizeFormMethod: false,
1653
1668
  v7_prependBasename: false
1654
1669
  }, init.future);
@@ -1768,6 +1783,10 @@ function createRouter(init) {
1768
1783
  // Most recent href/match for fetcher.load calls for fetchers
1769
1784
  let fetchLoadMatches = new Map();
1770
1785
 
1786
+ // Fetchers that have requested a delete when using v7_fetcherPersist,
1787
+ // they'll be officially removed after they return to idle
1788
+ let deletedFetchers = new Set();
1789
+
1771
1790
  // Store DeferredData instances for active route matches. When a
1772
1791
  // route loader returns defer() we stick one in here. Then, when a nested
1773
1792
  // promise resolves we update loaderData. If a new navigation starts we
@@ -1883,6 +1902,24 @@ function createRouter(init) {
1883
1902
  subscribers.forEach(subscriber => subscriber(state, {
1884
1903
  unstable_viewTransitionOpts: viewTransitionOpts
1885
1904
  }));
1905
+
1906
+ // Remove idle fetchers from state since we only care about in-flight fetchers.
1907
+ if (future.v7_fetcherPersist) {
1908
+ state.fetchers.forEach((fetcher, key) => {
1909
+ if (fetcher.state === "idle") {
1910
+ if (deletedFetchers.has(key)) {
1911
+ // If the fetcher has unmounted and called router.deleteFetcher(),
1912
+ // we can totally delete the fetcher
1913
+ deleteFetcher(key);
1914
+ } else {
1915
+ // Otherwise, it must still be mounted in the UI so we just remove
1916
+ // it from state now that we've handed off the data to the React
1917
+ // layer. Things such as fetchLoadMatches remain for revalidation.
1918
+ state.fetchers.delete(key);
1919
+ }
1920
+ }
1921
+ });
1922
+ }
1886
1923
  }
1887
1924
 
1888
1925
  // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
@@ -2624,7 +2661,7 @@ function createRouter(init) {
2624
2661
  let doneFetcher = getDoneFetcher(actionResult.data);
2625
2662
  state.fetchers.set(key, doneFetcher);
2626
2663
  }
2627
- let didAbortFetchLoads = abortStaleFetchLoads(loadId);
2664
+ abortStaleFetchLoads(loadId);
2628
2665
 
2629
2666
  // If we are currently in a navigation loading state and this fetcher is
2630
2667
  // more recent than the navigation, we want the newer data so abort the
@@ -2642,12 +2679,11 @@ function createRouter(init) {
2642
2679
  // otherwise just update with the fetcher data, preserving any existing
2643
2680
  // loaderData for loaders that did not need to reload. We have to
2644
2681
  // manually merge here since we aren't going through completeNavigation
2645
- updateState(_extends({
2682
+ updateState({
2646
2683
  errors,
2647
- loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors)
2648
- }, didAbortFetchLoads || revalidatingFetchers.length > 0 ? {
2684
+ loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors),
2649
2685
  fetchers: new Map(state.fetchers)
2650
- } : {}));
2686
+ });
2651
2687
  isRevalidationRequired = false;
2652
2688
  }
2653
2689
  }
@@ -2888,8 +2924,19 @@ function createRouter(init) {
2888
2924
  fetchLoadMatches.delete(key);
2889
2925
  fetchReloadIds.delete(key);
2890
2926
  fetchRedirectIds.delete(key);
2927
+ deletedFetchers.delete(key);
2891
2928
  state.fetchers.delete(key);
2892
2929
  }
2930
+ function deleteFetcherAndUpdateState(key) {
2931
+ if (future.v7_fetcherPersist) {
2932
+ deletedFetchers.add(key);
2933
+ } else {
2934
+ deleteFetcher(key);
2935
+ }
2936
+ updateState({
2937
+ fetchers: new Map(state.fetchers)
2938
+ });
2939
+ }
2893
2940
  function abortFetcher(key) {
2894
2941
  let controller = fetchControllers.get(key);
2895
2942
  invariant(controller, "Expected fetch controller: " + key);
@@ -3084,7 +3131,7 @@ function createRouter(init) {
3084
3131
  createHref: to => init.history.createHref(to),
3085
3132
  encodeLocation: to => init.history.encodeLocation(to),
3086
3133
  getFetcher,
3087
- deleteFetcher,
3134
+ deleteFetcher: deleteFetcherAndUpdateState,
3088
3135
  dispose,
3089
3136
  getBlocker,
3090
3137
  deleteBlocker,