@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/CHANGELOG.md +27 -0
- package/dist/router.cjs.js +109 -33
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +4 -2
- package/dist/router.js +105 -33
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +109 -33
- package/dist/router.umd.js.map +1 -1
- package/dist/router.umd.min.js +2 -2
- package/dist/router.umd.min.js.map +1 -1
- package/package.json +1 -1
- package/router.ts +84 -20
- package/utils.ts +19 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# `@remix-run/router`
|
|
2
2
|
|
|
3
|
+
## 1.11.0-pre.1
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- When `v7_fetcherPersist` is enabled, the router now performs ref-counting on fetcher keys via `getFetcher`/`deleteFetcher` so it knows when a given fetcher is totally unmounted from the UI ([#10977](https://github.com/remix-run/react-router/pull/10977))
|
|
8
|
+
|
|
9
|
+
- Once a fetcher has been totally unmounted, we can ignore post-processing of a persisted fetcher result such as a redirect or an error
|
|
10
|
+
- The router will also pass a new `deletedFetchers` array to the subscriber callbacks so that the UI layer can remove associated fetcher data
|
|
11
|
+
|
|
12
|
+
## 1.11.0-pre.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 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))
|
|
17
|
+
|
|
18
|
+
- 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
|
|
19
|
+
- 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)
|
|
20
|
+
- Keep an eye out for the following specific behavioral changes when opting into this flag and check your app for compatibility:
|
|
21
|
+
- 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`).
|
|
22
|
+
- 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.
|
|
23
|
+
|
|
24
|
+
- Add support for optional path segments in `matchPath` ([#10768](https://github.com/remix-run/react-router/pull/10768))
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 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))
|
|
29
|
+
|
|
3
30
|
## 1.10.0
|
|
4
31
|
|
|
5
32
|
### Minor Changes
|
package/dist/router.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @remix-run/router v1.
|
|
2
|
+
* @remix-run/router v1.11.0-pre.1
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(/[
|
|
1055
|
-
.replace(/\/:(\w+)
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
1306
|
-
let [key, value] =
|
|
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,
|
|
1407
|
-
let [key, value] =
|
|
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,13 @@ function createRouter(init) {
|
|
|
1768
1783
|
// Most recent href/match for fetcher.load calls for fetchers
|
|
1769
1784
|
let fetchLoadMatches = new Map();
|
|
1770
1785
|
|
|
1786
|
+
// Ref-count mounted fetchers so we know when it's ok to clean them up
|
|
1787
|
+
let activeFetchers = new Map();
|
|
1788
|
+
|
|
1789
|
+
// Fetchers that have requested a delete when using v7_fetcherPersist,
|
|
1790
|
+
// they'll be officially removed after they return to idle
|
|
1791
|
+
let deletedFetchers = new Set();
|
|
1792
|
+
|
|
1771
1793
|
// Store DeferredData instances for active route matches. When a
|
|
1772
1794
|
// route loader returns defer() we stick one in here. Then, when a nested
|
|
1773
1795
|
// promise resolves we update loaderData. If a new navigation starts we
|
|
@@ -1880,9 +1902,35 @@ function createRouter(init) {
|
|
|
1880
1902
|
// Update our state and notify the calling context of the change
|
|
1881
1903
|
function updateState(newState, viewTransitionOpts) {
|
|
1882
1904
|
state = _extends({}, state, newState);
|
|
1905
|
+
|
|
1906
|
+
// Prep fetcher cleanup so we can tell the UI which fetcher data entries
|
|
1907
|
+
// can be removed
|
|
1908
|
+
let completedFetchers = [];
|
|
1909
|
+
let deletedFetchersKeys = [];
|
|
1910
|
+
if (future.v7_fetcherPersist) {
|
|
1911
|
+
state.fetchers.forEach((fetcher, key) => {
|
|
1912
|
+
if (fetcher.state === "idle") {
|
|
1913
|
+
if (deletedFetchers.has(key)) {
|
|
1914
|
+
// Unmounted from the UI and can be totally removed
|
|
1915
|
+
deletedFetchersKeys.push(key);
|
|
1916
|
+
} else {
|
|
1917
|
+
// Returned to idle but still mounted in the UI, so semi-remains for
|
|
1918
|
+
// revalidations and such
|
|
1919
|
+
completedFetchers.push(key);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1883
1924
|
subscribers.forEach(subscriber => subscriber(state, {
|
|
1925
|
+
deletedFetchers: deletedFetchersKeys,
|
|
1884
1926
|
unstable_viewTransitionOpts: viewTransitionOpts
|
|
1885
1927
|
}));
|
|
1928
|
+
|
|
1929
|
+
// Remove idle fetchers from state since we only care about in-flight fetchers.
|
|
1930
|
+
if (future.v7_fetcherPersist) {
|
|
1931
|
+
completedFetchers.forEach(key => state.fetchers.delete(key));
|
|
1932
|
+
deletedFetchersKeys.forEach(key => deleteFetcher(key));
|
|
1933
|
+
}
|
|
1886
1934
|
}
|
|
1887
1935
|
|
|
1888
1936
|
// Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
|
|
@@ -2435,6 +2483,14 @@ function createRouter(init) {
|
|
|
2435
2483
|
} : {});
|
|
2436
2484
|
}
|
|
2437
2485
|
function getFetcher(key) {
|
|
2486
|
+
if (future.v7_fetcherPersist) {
|
|
2487
|
+
activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
|
|
2488
|
+
// If this fetcher was previously marked for deletion, unmark it since we
|
|
2489
|
+
// have a new instance
|
|
2490
|
+
if (deletedFetchers.has(key)) {
|
|
2491
|
+
deletedFetchers.delete(key);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2438
2494
|
return state.fetchers.get(key) || IDLE_FETCHER;
|
|
2439
2495
|
}
|
|
2440
2496
|
|
|
@@ -2508,13 +2564,20 @@ function createRouter(init) {
|
|
|
2508
2564
|
let originatingLoadId = incrementingLoadId;
|
|
2509
2565
|
let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
|
|
2510
2566
|
if (fetchRequest.signal.aborted) {
|
|
2511
|
-
// We can delete this so long as we weren't aborted by
|
|
2567
|
+
// We can delete this so long as we weren't aborted by our own fetcher
|
|
2512
2568
|
// re-submit which would have put _new_ controller is in fetchControllers
|
|
2513
2569
|
if (fetchControllers.get(key) === abortController) {
|
|
2514
2570
|
fetchControllers.delete(key);
|
|
2515
2571
|
}
|
|
2516
2572
|
return;
|
|
2517
2573
|
}
|
|
2574
|
+
if (deletedFetchers.has(key)) {
|
|
2575
|
+
state.fetchers.set(key, getDoneFetcher(undefined));
|
|
2576
|
+
updateState({
|
|
2577
|
+
fetchers: new Map(state.fetchers)
|
|
2578
|
+
});
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2518
2581
|
if (isRedirectResult(actionResult)) {
|
|
2519
2582
|
fetchControllers.delete(key);
|
|
2520
2583
|
if (pendingNavigationLoadId > originatingLoadId) {
|
|
@@ -2624,7 +2687,7 @@ function createRouter(init) {
|
|
|
2624
2687
|
let doneFetcher = getDoneFetcher(actionResult.data);
|
|
2625
2688
|
state.fetchers.set(key, doneFetcher);
|
|
2626
2689
|
}
|
|
2627
|
-
|
|
2690
|
+
abortStaleFetchLoads(loadId);
|
|
2628
2691
|
|
|
2629
2692
|
// If we are currently in a navigation loading state and this fetcher is
|
|
2630
2693
|
// more recent than the navigation, we want the newer data so abort the
|
|
@@ -2642,12 +2705,11 @@ function createRouter(init) {
|
|
|
2642
2705
|
// otherwise just update with the fetcher data, preserving any existing
|
|
2643
2706
|
// loaderData for loaders that did not need to reload. We have to
|
|
2644
2707
|
// manually merge here since we aren't going through completeNavigation
|
|
2645
|
-
updateState(
|
|
2708
|
+
updateState({
|
|
2646
2709
|
errors,
|
|
2647
|
-
loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors)
|
|
2648
|
-
}, didAbortFetchLoads || revalidatingFetchers.length > 0 ? {
|
|
2710
|
+
loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors),
|
|
2649
2711
|
fetchers: new Map(state.fetchers)
|
|
2650
|
-
}
|
|
2712
|
+
});
|
|
2651
2713
|
isRevalidationRequired = false;
|
|
2652
2714
|
}
|
|
2653
2715
|
}
|
|
@@ -2685,6 +2747,13 @@ function createRouter(init) {
|
|
|
2685
2747
|
if (fetchRequest.signal.aborted) {
|
|
2686
2748
|
return;
|
|
2687
2749
|
}
|
|
2750
|
+
if (deletedFetchers.has(key)) {
|
|
2751
|
+
state.fetchers.set(key, getDoneFetcher(undefined));
|
|
2752
|
+
updateState({
|
|
2753
|
+
fetchers: new Map(state.fetchers)
|
|
2754
|
+
});
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2688
2757
|
|
|
2689
2758
|
// If the loader threw a redirect Response, start a new REPLACE navigation
|
|
2690
2759
|
if (isRedirectResult(result)) {
|
|
@@ -2706,17 +2775,7 @@ function createRouter(init) {
|
|
|
2706
2775
|
|
|
2707
2776
|
// Process any non-redirect errors thrown
|
|
2708
2777
|
if (isErrorResult(result)) {
|
|
2709
|
-
|
|
2710
|
-
state.fetchers.delete(key);
|
|
2711
|
-
// TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
|
|
2712
|
-
// do we need to behave any differently with our non-redirect errors?
|
|
2713
|
-
// What if it was a non-redirect Response?
|
|
2714
|
-
updateState({
|
|
2715
|
-
fetchers: new Map(state.fetchers),
|
|
2716
|
-
errors: {
|
|
2717
|
-
[boundaryMatch.route.id]: result.error
|
|
2718
|
-
}
|
|
2719
|
-
});
|
|
2778
|
+
setFetcherError(key, routeId, result.error);
|
|
2720
2779
|
return;
|
|
2721
2780
|
}
|
|
2722
2781
|
invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
|
|
@@ -2888,8 +2947,25 @@ function createRouter(init) {
|
|
|
2888
2947
|
fetchLoadMatches.delete(key);
|
|
2889
2948
|
fetchReloadIds.delete(key);
|
|
2890
2949
|
fetchRedirectIds.delete(key);
|
|
2950
|
+
deletedFetchers.delete(key);
|
|
2891
2951
|
state.fetchers.delete(key);
|
|
2892
2952
|
}
|
|
2953
|
+
function deleteFetcherAndUpdateState(key) {
|
|
2954
|
+
if (future.v7_fetcherPersist) {
|
|
2955
|
+
let count = (activeFetchers.get(key) || 0) - 1;
|
|
2956
|
+
if (count <= 0) {
|
|
2957
|
+
activeFetchers.delete(key);
|
|
2958
|
+
deletedFetchers.add(key);
|
|
2959
|
+
} else {
|
|
2960
|
+
activeFetchers.set(key, count);
|
|
2961
|
+
}
|
|
2962
|
+
} else {
|
|
2963
|
+
deleteFetcher(key);
|
|
2964
|
+
}
|
|
2965
|
+
updateState({
|
|
2966
|
+
fetchers: new Map(state.fetchers)
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2893
2969
|
function abortFetcher(key) {
|
|
2894
2970
|
let controller = fetchControllers.get(key);
|
|
2895
2971
|
invariant(controller, "Expected fetch controller: " + key);
|
|
@@ -3084,7 +3160,7 @@ function createRouter(init) {
|
|
|
3084
3160
|
createHref: to => init.history.createHref(to),
|
|
3085
3161
|
encodeLocation: to => init.history.encodeLocation(to),
|
|
3086
3162
|
getFetcher,
|
|
3087
|
-
deleteFetcher,
|
|
3163
|
+
deleteFetcher: deleteFetcherAndUpdateState,
|
|
3088
3164
|
dispose,
|
|
3089
3165
|
getBlocker,
|
|
3090
3166
|
deleteBlocker,
|