@remix-run/router 0.0.0-experimental-65d6b8c9 → 0.0.0-experimental-e192105b
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 +22 -0
- package/dist/router.cjs.js +189 -77
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +12 -0
- package/dist/router.js +184 -76
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +189 -77
- 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 +218 -89
- package/utils.ts +19 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# `@remix-run/router`
|
|
2
2
|
|
|
3
|
+
## 1.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
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
|
+
|
|
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
|
+
- Keep an eye out for the following specific behavioral changes when opting into this flag and check your app for compatibility:
|
|
11
|
+
- 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`).
|
|
12
|
+
- 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.
|
|
13
|
+
|
|
14
|
+
- 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))
|
|
15
|
+
|
|
16
|
+
- Once a fetcher has been totally unmounted, we can ignore post-processing of a persisted fetcher result such as a redirect or an error
|
|
17
|
+
- The router will also pass a new `deletedFetchers` array to the subscriber callbacks so that the UI layer can remove associated fetcher data
|
|
18
|
+
|
|
19
|
+
- Add support for optional path segments in `matchPath` ([#10768](https://github.com/remix-run/react-router/pull/10768))
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 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))
|
|
24
|
+
|
|
3
25
|
## 1.10.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/dist/router.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @remix-run/router v0.0.0-experimental-
|
|
2
|
+
* @remix-run/router v0.0.0-experimental-e192105b
|
|
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);
|
|
@@ -1724,6 +1739,9 @@ function createRouter(init) {
|
|
|
1724
1739
|
// Should the current navigation enable document.startViewTransition?
|
|
1725
1740
|
let pendingViewTransitionEnabled = false;
|
|
1726
1741
|
|
|
1742
|
+
// Should the current navigation use flushSync for state updates?
|
|
1743
|
+
let pendingFlushSync = false;
|
|
1744
|
+
|
|
1727
1745
|
// Store applied view transitions so we can apply them on POP
|
|
1728
1746
|
let appliedViewTransitions = new Map();
|
|
1729
1747
|
|
|
@@ -1768,6 +1786,13 @@ function createRouter(init) {
|
|
|
1768
1786
|
// Most recent href/match for fetcher.load calls for fetchers
|
|
1769
1787
|
let fetchLoadMatches = new Map();
|
|
1770
1788
|
|
|
1789
|
+
// Ref-count mounted fetchers so we know when it's ok to clean them up
|
|
1790
|
+
let activeFetchers = new Map();
|
|
1791
|
+
|
|
1792
|
+
// Fetchers that have requested a delete when using v7_fetcherPersist,
|
|
1793
|
+
// they'll be officially removed after they return to idle
|
|
1794
|
+
let deletedFetchers = new Set();
|
|
1795
|
+
|
|
1771
1796
|
// Store DeferredData instances for active route matches. When a
|
|
1772
1797
|
// route loader returns defer() we stick one in here. Then, when a nested
|
|
1773
1798
|
// promise resolves we update loaderData. If a new navigation starts we
|
|
@@ -1830,6 +1855,8 @@ function createRouter(init) {
|
|
|
1830
1855
|
blockers.set(blockerKey, IDLE_BLOCKER);
|
|
1831
1856
|
updateState({
|
|
1832
1857
|
blockers
|
|
1858
|
+
}, {
|
|
1859
|
+
flushSync: false
|
|
1833
1860
|
});
|
|
1834
1861
|
}
|
|
1835
1862
|
});
|
|
@@ -1878,20 +1905,42 @@ function createRouter(init) {
|
|
|
1878
1905
|
}
|
|
1879
1906
|
|
|
1880
1907
|
// Update our state and notify the calling context of the change
|
|
1881
|
-
function updateState(newState,
|
|
1908
|
+
function updateState(newState, opts) {
|
|
1882
1909
|
state = _extends({}, state, newState);
|
|
1883
|
-
|
|
1884
|
-
|
|
1910
|
+
|
|
1911
|
+
// Prep fetcher cleanup so we can tell the UI which fetcher data entries
|
|
1912
|
+
// can be removed
|
|
1913
|
+
let completedFetchers = [];
|
|
1914
|
+
let deletedFetchersKeys = [];
|
|
1915
|
+
if (future.v7_fetcherPersist) {
|
|
1916
|
+
state.fetchers.forEach((fetcher, key) => {
|
|
1917
|
+
if (fetcher.state === "idle") {
|
|
1918
|
+
if (deletedFetchers.has(key)) {
|
|
1919
|
+
// Unmounted from the UI and can be totally removed
|
|
1920
|
+
deletedFetchersKeys.push(key);
|
|
1921
|
+
} else {
|
|
1922
|
+
// Returned to idle but still mounted in the UI, so semi-remains for
|
|
1923
|
+
// revalidations and such
|
|
1924
|
+
completedFetchers.push(key);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// Iterate over a local copy so that if flushSync is used and we end up
|
|
1931
|
+
// removing and adding a new subscriber due to the useCallback dependencies,
|
|
1932
|
+
// we don't get ourselves into a loop calling the new subscriber immediately
|
|
1933
|
+
[...subscribers].forEach(subscriber => subscriber(state, {
|
|
1934
|
+
deletedFetchers: deletedFetchersKeys,
|
|
1935
|
+
unstable_viewTransitionOpts: opts.viewTransitionOpts,
|
|
1936
|
+
unstable_flushSync: opts.flushSync
|
|
1885
1937
|
}));
|
|
1886
1938
|
|
|
1887
1939
|
// Remove idle fetchers from state since we only care about in-flight fetchers.
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
state.fetchers.delete(key);
|
|
1893
|
-
}
|
|
1894
|
-
});
|
|
1940
|
+
if (future.v7_fetcherPersist) {
|
|
1941
|
+
completedFetchers.forEach(key => state.fetchers.delete(key));
|
|
1942
|
+
deletedFetchersKeys.forEach(key => deleteFetcher(key));
|
|
1943
|
+
}
|
|
1895
1944
|
}
|
|
1896
1945
|
|
|
1897
1946
|
// Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
|
|
@@ -1991,12 +2040,16 @@ function createRouter(init) {
|
|
|
1991
2040
|
restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
|
|
1992
2041
|
preventScrollReset,
|
|
1993
2042
|
blockers
|
|
1994
|
-
}),
|
|
2043
|
+
}), {
|
|
2044
|
+
viewTransitionOpts,
|
|
2045
|
+
flushSync: pendingFlushSync
|
|
2046
|
+
});
|
|
1995
2047
|
|
|
1996
2048
|
// Reset stateful navigation vars
|
|
1997
2049
|
pendingAction = Action.Pop;
|
|
1998
2050
|
pendingPreventScrollReset = false;
|
|
1999
2051
|
pendingViewTransitionEnabled = false;
|
|
2052
|
+
pendingFlushSync = false;
|
|
2000
2053
|
isUninterruptedRevalidation = false;
|
|
2001
2054
|
isRevalidationRequired = false;
|
|
2002
2055
|
cancelledDeferredRoutes = [];
|
|
@@ -2037,6 +2090,7 @@ function createRouter(init) {
|
|
|
2037
2090
|
historyAction = Action.Replace;
|
|
2038
2091
|
}
|
|
2039
2092
|
let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
|
|
2093
|
+
let flushSync = (opts && opts.unstable_flushSync) === true;
|
|
2040
2094
|
let blockerKey = shouldBlockNavigation({
|
|
2041
2095
|
currentLocation,
|
|
2042
2096
|
nextLocation,
|
|
@@ -2062,6 +2116,8 @@ function createRouter(init) {
|
|
|
2062
2116
|
blockers.set(blockerKey, IDLE_BLOCKER);
|
|
2063
2117
|
updateState({
|
|
2064
2118
|
blockers
|
|
2119
|
+
}, {
|
|
2120
|
+
flushSync
|
|
2065
2121
|
});
|
|
2066
2122
|
}
|
|
2067
2123
|
});
|
|
@@ -2074,7 +2130,8 @@ function createRouter(init) {
|
|
|
2074
2130
|
pendingError: error,
|
|
2075
2131
|
preventScrollReset,
|
|
2076
2132
|
replace: opts && opts.replace,
|
|
2077
|
-
enableViewTransition: opts && opts.unstable_viewTransition
|
|
2133
|
+
enableViewTransition: opts && opts.unstable_viewTransition,
|
|
2134
|
+
flushSync
|
|
2078
2135
|
});
|
|
2079
2136
|
}
|
|
2080
2137
|
|
|
@@ -2085,6 +2142,8 @@ function createRouter(init) {
|
|
|
2085
2142
|
interruptActiveLoads();
|
|
2086
2143
|
updateState({
|
|
2087
2144
|
revalidation: "loading"
|
|
2145
|
+
}, {
|
|
2146
|
+
flushSync: false
|
|
2088
2147
|
});
|
|
2089
2148
|
|
|
2090
2149
|
// If we're currently submitting an action, we don't need to start a new
|
|
@@ -2128,6 +2187,7 @@ function createRouter(init) {
|
|
|
2128
2187
|
saveScrollPosition(state.location, state.matches);
|
|
2129
2188
|
pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
|
|
2130
2189
|
pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
|
|
2190
|
+
pendingFlushSync = (opts && opts.flushSync) === true;
|
|
2131
2191
|
let routesToUse = inFlightDataRoutes || dataRoutes;
|
|
2132
2192
|
let loadingNavigation = opts && opts.overrideNavigation;
|
|
2133
2193
|
let matches = matchRoutes(routesToUse, location, basename);
|
|
@@ -2233,6 +2293,8 @@ function createRouter(init) {
|
|
|
2233
2293
|
let navigation = getSubmittingNavigation(location, submission);
|
|
2234
2294
|
updateState({
|
|
2235
2295
|
navigation
|
|
2296
|
+
}, {
|
|
2297
|
+
flushSync: pendingFlushSync
|
|
2236
2298
|
});
|
|
2237
2299
|
|
|
2238
2300
|
// Call our action and get the result
|
|
@@ -2360,7 +2422,9 @@ function createRouter(init) {
|
|
|
2360
2422
|
actionData
|
|
2361
2423
|
} : {}, revalidatingFetchers.length > 0 ? {
|
|
2362
2424
|
fetchers: new Map(state.fetchers)
|
|
2363
|
-
} : {})
|
|
2425
|
+
} : {}), {
|
|
2426
|
+
flushSync: pendingFlushSync
|
|
2427
|
+
});
|
|
2364
2428
|
}
|
|
2365
2429
|
revalidatingFetchers.forEach(rf => {
|
|
2366
2430
|
if (fetchControllers.has(rf.key)) {
|
|
@@ -2450,13 +2514,16 @@ function createRouter(init) {
|
|
|
2450
2514
|
throw new Error("router.fetch() was called during the server render, but it shouldn't be. " + "You are likely calling a useFetcher() method in the body of your component. " + "Try moving it to a useEffect or a callback.");
|
|
2451
2515
|
}
|
|
2452
2516
|
if (fetchControllers.has(key)) abortFetcher(key);
|
|
2517
|
+
let flushSync = (opts && opts.unstable_flushSync) === true;
|
|
2453
2518
|
let routesToUse = inFlightDataRoutes || dataRoutes;
|
|
2454
2519
|
let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, routeId, opts == null ? void 0 : opts.relative);
|
|
2455
2520
|
let matches = matchRoutes(routesToUse, normalizedPath, basename);
|
|
2456
2521
|
if (!matches) {
|
|
2457
2522
|
setFetcherError(key, routeId, getInternalRouterError(404, {
|
|
2458
2523
|
pathname: normalizedPath
|
|
2459
|
-
})
|
|
2524
|
+
}), {
|
|
2525
|
+
flushSync
|
|
2526
|
+
});
|
|
2460
2527
|
return;
|
|
2461
2528
|
}
|
|
2462
2529
|
let {
|
|
@@ -2465,13 +2532,15 @@ function createRouter(init) {
|
|
|
2465
2532
|
error
|
|
2466
2533
|
} = normalizeNavigateOptions(future.v7_normalizeFormMethod, true, normalizedPath, opts);
|
|
2467
2534
|
if (error) {
|
|
2468
|
-
setFetcherError(key, routeId, error
|
|
2535
|
+
setFetcherError(key, routeId, error, {
|
|
2536
|
+
flushSync
|
|
2537
|
+
});
|
|
2469
2538
|
return;
|
|
2470
2539
|
}
|
|
2471
2540
|
let match = getTargetMatch(matches, path);
|
|
2472
2541
|
pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
|
|
2473
2542
|
if (submission && isMutationMethod(submission.formMethod)) {
|
|
2474
|
-
handleFetcherAction(key, routeId, path, match, matches, submission);
|
|
2543
|
+
handleFetcherAction(key, routeId, path, match, matches, flushSync, submission);
|
|
2475
2544
|
return;
|
|
2476
2545
|
}
|
|
2477
2546
|
|
|
@@ -2481,12 +2550,12 @@ function createRouter(init) {
|
|
|
2481
2550
|
routeId,
|
|
2482
2551
|
path
|
|
2483
2552
|
});
|
|
2484
|
-
handleFetcherLoader(key, routeId, path, match, matches, submission);
|
|
2553
|
+
handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission);
|
|
2485
2554
|
}
|
|
2486
2555
|
|
|
2487
2556
|
// Call the action for the matched fetcher.submit(), and then handle redirects,
|
|
2488
2557
|
// errors, and revalidation
|
|
2489
|
-
async function handleFetcherAction(key, routeId, path, match, requestMatches, submission) {
|
|
2558
|
+
async function handleFetcherAction(key, routeId, path, match, requestMatches, flushSync, submission) {
|
|
2490
2559
|
interruptActiveLoads();
|
|
2491
2560
|
fetchLoadMatches.delete(key);
|
|
2492
2561
|
if (!match.route.action && !match.route.lazy) {
|
|
@@ -2495,16 +2564,16 @@ function createRouter(init) {
|
|
|
2495
2564
|
pathname: path,
|
|
2496
2565
|
routeId: routeId
|
|
2497
2566
|
});
|
|
2498
|
-
setFetcherError(key, routeId, error
|
|
2567
|
+
setFetcherError(key, routeId, error, {
|
|
2568
|
+
flushSync
|
|
2569
|
+
});
|
|
2499
2570
|
return;
|
|
2500
2571
|
}
|
|
2501
2572
|
|
|
2502
2573
|
// Put this fetcher into it's submitting state
|
|
2503
2574
|
let existingFetcher = state.fetchers.get(key);
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
updateState({
|
|
2507
|
-
fetchers: new Map(state.fetchers)
|
|
2575
|
+
updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
|
|
2576
|
+
flushSync
|
|
2508
2577
|
});
|
|
2509
2578
|
|
|
2510
2579
|
// Call the action for the fetcher
|
|
@@ -2514,13 +2583,19 @@ function createRouter(init) {
|
|
|
2514
2583
|
let originatingLoadId = incrementingLoadId;
|
|
2515
2584
|
let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
|
|
2516
2585
|
if (fetchRequest.signal.aborted) {
|
|
2517
|
-
// We can delete this so long as we weren't aborted by
|
|
2586
|
+
// We can delete this so long as we weren't aborted by our own fetcher
|
|
2518
2587
|
// re-submit which would have put _new_ controller is in fetchControllers
|
|
2519
2588
|
if (fetchControllers.get(key) === abortController) {
|
|
2520
2589
|
fetchControllers.delete(key);
|
|
2521
2590
|
}
|
|
2522
2591
|
return;
|
|
2523
2592
|
}
|
|
2593
|
+
if (deletedFetchers.has(key)) {
|
|
2594
|
+
updateFetcherState(key, getDoneFetcher(undefined), {
|
|
2595
|
+
flushSync
|
|
2596
|
+
});
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2524
2599
|
if (isRedirectResult(actionResult)) {
|
|
2525
2600
|
fetchControllers.delete(key);
|
|
2526
2601
|
if (pendingNavigationLoadId > originatingLoadId) {
|
|
@@ -2528,18 +2603,14 @@ function createRouter(init) {
|
|
|
2528
2603
|
// should take precedence over this redirect navigation. We already
|
|
2529
2604
|
// set isRevalidationRequired so all loaders for the new route should
|
|
2530
2605
|
// fire unless opted out via shouldRevalidate
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
updateState({
|
|
2534
|
-
fetchers: new Map(state.fetchers)
|
|
2606
|
+
updateFetcherState(key, getDoneFetcher(undefined), {
|
|
2607
|
+
flushSync
|
|
2535
2608
|
});
|
|
2536
2609
|
return;
|
|
2537
2610
|
} else {
|
|
2538
2611
|
fetchRedirectIds.add(key);
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
updateState({
|
|
2542
|
-
fetchers: new Map(state.fetchers)
|
|
2612
|
+
updateFetcherState(key, getLoadingFetcher(submission), {
|
|
2613
|
+
flushSync
|
|
2543
2614
|
});
|
|
2544
2615
|
return startRedirectNavigation(state, actionResult, {
|
|
2545
2616
|
fetcherSubmission: submission
|
|
@@ -2549,7 +2620,9 @@ function createRouter(init) {
|
|
|
2549
2620
|
|
|
2550
2621
|
// Process any non-redirect errors thrown
|
|
2551
2622
|
if (isErrorResult(actionResult)) {
|
|
2552
|
-
setFetcherError(key, routeId, actionResult.error
|
|
2623
|
+
setFetcherError(key, routeId, actionResult.error, {
|
|
2624
|
+
flushSync
|
|
2625
|
+
});
|
|
2553
2626
|
return;
|
|
2554
2627
|
}
|
|
2555
2628
|
if (isDeferredResult(actionResult)) {
|
|
@@ -2591,6 +2664,8 @@ function createRouter(init) {
|
|
|
2591
2664
|
});
|
|
2592
2665
|
updateState({
|
|
2593
2666
|
fetchers: new Map(state.fetchers)
|
|
2667
|
+
}, {
|
|
2668
|
+
flushSync
|
|
2594
2669
|
});
|
|
2595
2670
|
let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(rf => abortFetcher(rf.key));
|
|
2596
2671
|
abortController.signal.addEventListener("abort", abortPendingFetchRevalidations);
|
|
@@ -2652,19 +2727,18 @@ function createRouter(init) {
|
|
|
2652
2727
|
errors,
|
|
2653
2728
|
loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors),
|
|
2654
2729
|
fetchers: new Map(state.fetchers)
|
|
2730
|
+
}, {
|
|
2731
|
+
flushSync
|
|
2655
2732
|
});
|
|
2656
2733
|
isRevalidationRequired = false;
|
|
2657
2734
|
}
|
|
2658
2735
|
}
|
|
2659
2736
|
|
|
2660
2737
|
// Call the matched loader for fetcher.load(), handling redirects, errors, etc.
|
|
2661
|
-
async function handleFetcherLoader(key, routeId, path, match, matches, submission) {
|
|
2738
|
+
async function handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission) {
|
|
2662
2739
|
let existingFetcher = state.fetchers.get(key);
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
state.fetchers.set(key, loadingFetcher);
|
|
2666
|
-
updateState({
|
|
2667
|
-
fetchers: new Map(state.fetchers)
|
|
2740
|
+
updateFetcherState(key, getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined), {
|
|
2741
|
+
flushSync
|
|
2668
2742
|
});
|
|
2669
2743
|
|
|
2670
2744
|
// Call the loader for this fetcher route match
|
|
@@ -2690,16 +2764,20 @@ function createRouter(init) {
|
|
|
2690
2764
|
if (fetchRequest.signal.aborted) {
|
|
2691
2765
|
return;
|
|
2692
2766
|
}
|
|
2767
|
+
if (deletedFetchers.has(key)) {
|
|
2768
|
+
updateFetcherState(key, getDoneFetcher(undefined), {
|
|
2769
|
+
flushSync
|
|
2770
|
+
});
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2693
2773
|
|
|
2694
2774
|
// If the loader threw a redirect Response, start a new REPLACE navigation
|
|
2695
2775
|
if (isRedirectResult(result)) {
|
|
2696
2776
|
if (pendingNavigationLoadId > originatingLoadId) {
|
|
2697
2777
|
// A new navigation was kicked off after our loader started, so that
|
|
2698
2778
|
// should take precedence over this redirect navigation
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
updateState({
|
|
2702
|
-
fetchers: new Map(state.fetchers)
|
|
2779
|
+
updateFetcherState(key, getDoneFetcher(undefined), {
|
|
2780
|
+
flushSync
|
|
2703
2781
|
});
|
|
2704
2782
|
return;
|
|
2705
2783
|
} else {
|
|
@@ -2711,26 +2789,16 @@ function createRouter(init) {
|
|
|
2711
2789
|
|
|
2712
2790
|
// Process any non-redirect errors thrown
|
|
2713
2791
|
if (isErrorResult(result)) {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
// TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
|
|
2717
|
-
// do we need to behave any differently with our non-redirect errors?
|
|
2718
|
-
// What if it was a non-redirect Response?
|
|
2719
|
-
updateState({
|
|
2720
|
-
fetchers: new Map(state.fetchers),
|
|
2721
|
-
errors: {
|
|
2722
|
-
[boundaryMatch.route.id]: result.error
|
|
2723
|
-
}
|
|
2792
|
+
setFetcherError(key, routeId, result.error, {
|
|
2793
|
+
flushSync
|
|
2724
2794
|
});
|
|
2725
2795
|
return;
|
|
2726
2796
|
}
|
|
2727
2797
|
invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
|
|
2728
2798
|
|
|
2729
2799
|
// Put the fetcher back into an idle state
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
updateState({
|
|
2733
|
-
fetchers: new Map(state.fetchers)
|
|
2800
|
+
updateFetcherState(key, getDoneFetcher(result.data), {
|
|
2801
|
+
flushSync
|
|
2734
2802
|
});
|
|
2735
2803
|
}
|
|
2736
2804
|
|
|
@@ -2872,7 +2940,15 @@ function createRouter(init) {
|
|
|
2872
2940
|
}
|
|
2873
2941
|
});
|
|
2874
2942
|
}
|
|
2875
|
-
function
|
|
2943
|
+
function updateFetcherState(key, fetcher, opts) {
|
|
2944
|
+
state.fetchers.set(key, fetcher);
|
|
2945
|
+
updateState({
|
|
2946
|
+
fetchers: new Map(state.fetchers)
|
|
2947
|
+
}, {
|
|
2948
|
+
flushSync: opts.flushSync
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
function setFetcherError(key, routeId, error, opts) {
|
|
2876
2952
|
let boundaryMatch = findNearestBoundary(state.matches, routeId);
|
|
2877
2953
|
deleteFetcher(key);
|
|
2878
2954
|
updateState({
|
|
@@ -2880,11 +2956,23 @@ function createRouter(init) {
|
|
|
2880
2956
|
[boundaryMatch.route.id]: error
|
|
2881
2957
|
},
|
|
2882
2958
|
fetchers: new Map(state.fetchers)
|
|
2959
|
+
}, {
|
|
2960
|
+
flushSync: opts.flushSync
|
|
2883
2961
|
});
|
|
2884
2962
|
}
|
|
2963
|
+
function getFetcher(key) {
|
|
2964
|
+
if (future.v7_fetcherPersist) {
|
|
2965
|
+
activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
|
|
2966
|
+
// If this fetcher was previously marked for deletion, unmark it since we
|
|
2967
|
+
// have a new instance
|
|
2968
|
+
if (deletedFetchers.has(key)) {
|
|
2969
|
+
deletedFetchers.delete(key);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
return state.fetchers.get(key) || IDLE_FETCHER;
|
|
2973
|
+
}
|
|
2885
2974
|
function deleteFetcher(key) {
|
|
2886
2975
|
let fetcher = state.fetchers.get(key);
|
|
2887
|
-
|
|
2888
2976
|
// Don't abort the controller if this is a deletion of a fetcher.submit()
|
|
2889
2977
|
// in it's loading phase since - we don't want to abort the corresponding
|
|
2890
2978
|
// revalidation and want them to complete and land
|
|
@@ -2894,8 +2982,27 @@ function createRouter(init) {
|
|
|
2894
2982
|
fetchLoadMatches.delete(key);
|
|
2895
2983
|
fetchReloadIds.delete(key);
|
|
2896
2984
|
fetchRedirectIds.delete(key);
|
|
2985
|
+
deletedFetchers.delete(key);
|
|
2897
2986
|
state.fetchers.delete(key);
|
|
2898
2987
|
}
|
|
2988
|
+
function deleteFetcherAndUpdateState(key) {
|
|
2989
|
+
if (future.v7_fetcherPersist) {
|
|
2990
|
+
let count = (activeFetchers.get(key) || 0) - 1;
|
|
2991
|
+
if (count <= 0) {
|
|
2992
|
+
activeFetchers.delete(key);
|
|
2993
|
+
deletedFetchers.add(key);
|
|
2994
|
+
} else {
|
|
2995
|
+
activeFetchers.set(key, count);
|
|
2996
|
+
}
|
|
2997
|
+
} else {
|
|
2998
|
+
deleteFetcher(key);
|
|
2999
|
+
}
|
|
3000
|
+
updateState({
|
|
3001
|
+
fetchers: new Map(state.fetchers)
|
|
3002
|
+
}, {
|
|
3003
|
+
flushSync: false
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
2899
3006
|
function abortFetcher(key) {
|
|
2900
3007
|
let controller = fetchControllers.get(key);
|
|
2901
3008
|
invariant(controller, "Expected fetch controller: " + key);
|
|
@@ -2904,8 +3011,8 @@ function createRouter(init) {
|
|
|
2904
3011
|
}
|
|
2905
3012
|
function markFetchersDone(keys) {
|
|
2906
3013
|
for (let key of keys) {
|
|
2907
|
-
let fetcher =
|
|
2908
|
-
let doneFetcher = getDoneFetcher(fetcher
|
|
3014
|
+
let fetcher = getFetcher(key);
|
|
3015
|
+
let doneFetcher = getDoneFetcher(fetcher.data);
|
|
2909
3016
|
state.fetchers.set(key, doneFetcher);
|
|
2910
3017
|
}
|
|
2911
3018
|
}
|
|
@@ -2963,6 +3070,8 @@ function createRouter(init) {
|
|
|
2963
3070
|
blockers.set(key, newBlocker);
|
|
2964
3071
|
updateState({
|
|
2965
3072
|
blockers
|
|
3073
|
+
}, {
|
|
3074
|
+
flushSync: false
|
|
2966
3075
|
});
|
|
2967
3076
|
}
|
|
2968
3077
|
function shouldBlockNavigation(_ref2) {
|
|
@@ -3030,6 +3139,8 @@ function createRouter(init) {
|
|
|
3030
3139
|
if (y != null) {
|
|
3031
3140
|
updateState({
|
|
3032
3141
|
restoreScrollPosition: y
|
|
3142
|
+
}, {
|
|
3143
|
+
flushSync: false
|
|
3033
3144
|
});
|
|
3034
3145
|
}
|
|
3035
3146
|
}
|
|
@@ -3089,7 +3200,8 @@ function createRouter(init) {
|
|
|
3089
3200
|
// hash-aware URLs in DOM paths
|
|
3090
3201
|
createHref: to => init.history.createHref(to),
|
|
3091
3202
|
encodeLocation: to => init.history.encodeLocation(to),
|
|
3092
|
-
|
|
3203
|
+
getFetcher,
|
|
3204
|
+
deleteFetcher: deleteFetcherAndUpdateState,
|
|
3093
3205
|
dispose,
|
|
3094
3206
|
getBlocker,
|
|
3095
3207
|
deleteBlocker,
|