@remix-run/router 1.7.1 → 1.7.2-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/dist/utils.d.ts CHANGED
@@ -191,12 +191,15 @@ export interface MapRoutePropertiesFunction {
191
191
  */
192
192
  export type ImmutableRouteKey = "lazy" | "caseSensitive" | "path" | "id" | "index" | "children";
193
193
  export declare const immutableRouteKeys: Set<ImmutableRouteKey>;
194
+ type RequireOne<T, Key = keyof T> = Exclude<{
195
+ [K in keyof T]: K extends Key ? Omit<T, K> & Required<Pick<T, K>> : never;
196
+ }[keyof T], undefined>;
194
197
  /**
195
198
  * lazy() function to load a route definition, which can add non-matching
196
199
  * related properties to a route
197
200
  */
198
201
  export interface LazyRouteFunction<R extends AgnosticRouteObject> {
199
- (): Promise<Omit<R, ImmutableRouteKey>>;
202
+ (): Promise<RequireOne<Omit<R, ImmutableRouteKey>>>;
200
203
  }
201
204
  /**
202
205
  * Base RouteObject with common props shared by all types of routes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.7.1",
3
+ "version": "1.7.2-pre.1",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -1500,6 +1500,8 @@ export function createRouter(init: RouterInit): Router {
1500
1500
  (matchesToLoad && matchesToLoad.some((m) => m.route.id === routeId))
1501
1501
  );
1502
1502
 
1503
+ pendingNavigationLoadId = ++incrementingLoadId;
1504
+
1503
1505
  // Short circuit if we have no loaders to run
1504
1506
  if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
1505
1507
  let updatedFetchers = markFetchRedirectsDone();
@@ -1541,7 +1543,6 @@ export function createRouter(init: RouterInit): Router {
1541
1543
  });
1542
1544
  }
1543
1545
 
1544
- pendingNavigationLoadId = ++incrementingLoadId;
1545
1546
  revalidatingFetchers.forEach((rf) => {
1546
1547
  if (fetchControllers.has(rf.key)) {
1547
1548
  abortFetcher(rf.key);
@@ -1591,7 +1592,15 @@ export function createRouter(init: RouterInit): Router {
1591
1592
  // If any loaders returned a redirect Response, start a new REPLACE navigation
1592
1593
  let redirect = findRedirect(results);
1593
1594
  if (redirect) {
1594
- await startRedirectNavigation(state, redirect, { replace });
1595
+ if (redirect.idx >= matchesToLoad.length) {
1596
+ // If this redirect came from a fetcher make sure we mark it in
1597
+ // fetchRedirectIds so it doesn't get revalidated on the next set of
1598
+ // loader executions
1599
+ let fetcherKey =
1600
+ revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
1601
+ fetchRedirectIds.add(fetcherKey);
1602
+ }
1603
+ await startRedirectNavigation(state, redirect.result, { replace });
1595
1604
  return { shortCircuited: true };
1596
1605
  }
1597
1606
 
@@ -1739,6 +1748,7 @@ export function createRouter(init: RouterInit): Router {
1739
1748
  );
1740
1749
  fetchControllers.set(key, abortController);
1741
1750
 
1751
+ let originatingLoadId = incrementingLoadId;
1742
1752
  let actionResult = await callLoaderOrAction(
1743
1753
  "action",
1744
1754
  fetchRequest,
@@ -1760,15 +1770,26 @@ export function createRouter(init: RouterInit): Router {
1760
1770
 
1761
1771
  if (isRedirectResult(actionResult)) {
1762
1772
  fetchControllers.delete(key);
1763
- fetchRedirectIds.add(key);
1764
- let loadingFetcher = getLoadingFetcher(submission);
1765
- state.fetchers.set(key, loadingFetcher);
1766
- updateState({ fetchers: new Map(state.fetchers) });
1767
-
1768
- return startRedirectNavigation(state, actionResult, {
1769
- submission,
1770
- isFetchActionRedirect: true,
1771
- });
1773
+ if (pendingNavigationLoadId > originatingLoadId) {
1774
+ // A new navigation was kicked off after our action started, so that
1775
+ // should take precedence over this redirect navigation. We already
1776
+ // set isRevalidationRequired so all loaders for the new route should
1777
+ // fire unless opted out via shouldRevalidate
1778
+ let doneFetcher = getDoneFetcher(undefined);
1779
+ state.fetchers.set(key, doneFetcher);
1780
+ updateState({ fetchers: new Map(state.fetchers) });
1781
+ return;
1782
+ } else {
1783
+ fetchRedirectIds.add(key);
1784
+ let loadingFetcher = getLoadingFetcher(submission);
1785
+ state.fetchers.set(key, loadingFetcher);
1786
+ updateState({ fetchers: new Map(state.fetchers) });
1787
+
1788
+ return startRedirectNavigation(state, actionResult, {
1789
+ submission,
1790
+ isFetchActionRedirect: true,
1791
+ });
1792
+ }
1772
1793
  }
1773
1794
 
1774
1795
  // Process any non-redirect errors thrown
@@ -1875,7 +1896,15 @@ export function createRouter(init: RouterInit): Router {
1875
1896
 
1876
1897
  let redirect = findRedirect(results);
1877
1898
  if (redirect) {
1878
- return startRedirectNavigation(state, redirect);
1899
+ if (redirect.idx >= matchesToLoad.length) {
1900
+ // If this redirect came from a fetcher make sure we mark it in
1901
+ // fetchRedirectIds so it doesn't get revalidated on the next set of
1902
+ // loader executions
1903
+ let fetcherKey =
1904
+ revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
1905
+ fetchRedirectIds.add(fetcherKey);
1906
+ }
1907
+ return startRedirectNavigation(state, redirect.result);
1879
1908
  }
1880
1909
 
1881
1910
  // Process and commit output from loaders
@@ -1962,6 +1991,7 @@ export function createRouter(init: RouterInit): Router {
1962
1991
  );
1963
1992
  fetchControllers.set(key, abortController);
1964
1993
 
1994
+ let originatingLoadId = incrementingLoadId;
1965
1995
  let result: DataResult = await callLoaderOrAction(
1966
1996
  "loader",
1967
1997
  fetchRequest,
@@ -1994,9 +2024,18 @@ export function createRouter(init: RouterInit): Router {
1994
2024
 
1995
2025
  // If the loader threw a redirect Response, start a new REPLACE navigation
1996
2026
  if (isRedirectResult(result)) {
1997
- fetchRedirectIds.add(key);
1998
- await startRedirectNavigation(state, result);
1999
- return;
2027
+ if (pendingNavigationLoadId > originatingLoadId) {
2028
+ // A new navigation was kicked off after our loader started, so that
2029
+ // should take precedence over this redirect navigation
2030
+ let doneFetcher = getDoneFetcher(undefined);
2031
+ state.fetchers.set(key, doneFetcher);
2032
+ updateState({ fetchers: new Map(state.fetchers) });
2033
+ return;
2034
+ } else {
2035
+ fetchRedirectIds.add(key);
2036
+ await startRedirectNavigation(state, result);
2037
+ return;
2038
+ }
2000
2039
  }
2001
2040
 
2002
2041
  // Process any non-redirect errors thrown
@@ -3360,7 +3399,9 @@ function getMatchesToLoad(
3360
3399
  let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
3361
3400
 
3362
3401
  // If the fetcher path no longer matches, push it in with null matches so
3363
- // we can trigger a 404 in callLoadersAndMaybeResolveData
3402
+ // we can trigger a 404 in callLoadersAndMaybeResolveData. Note this is
3403
+ // currently only a use-case for Remix HMR where the route tree can change
3404
+ // at runtime and remove a route previously loaded via a fetcher
3364
3405
  if (!fetcherMatches) {
3365
3406
  revalidatingFetchers.push({
3366
3407
  key,
@@ -3374,28 +3415,31 @@ function getMatchesToLoad(
3374
3415
  }
3375
3416
 
3376
3417
  // Revalidating fetchers are decoupled from the route matches since they
3377
- // load from a static href. They only set `defaultShouldRevalidate` on
3378
- // explicit revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3379
- //
3380
- // They automatically revalidate without even calling shouldRevalidate if:
3381
- // - They were cancelled
3382
- // - They're in the middle of their first load and therefore this is still
3383
- // an initial load and not a revalidation
3384
- //
3385
- // If neither of those is true, then they _always_ check shouldRevalidate
3418
+ // load from a static href. They revalidate based on explicit revalidation
3419
+ // (submission, useRevalidator, or X-Remix-Revalidate)
3386
3420
  let fetcher = state.fetchers.get(key);
3387
- let isPerformingInitialLoad =
3421
+ let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3422
+
3423
+ let shouldRevalidate = false;
3424
+ if (fetchRedirectIds.has(key)) {
3425
+ // Never trigger a revalidation of an actively redirecting fetcher
3426
+ shouldRevalidate = false;
3427
+ } else if (cancelledFetcherLoads.includes(key)) {
3428
+ // Always revalidate if the fetcher was cancelled
3429
+ shouldRevalidate = true;
3430
+ } else if (
3388
3431
  fetcher &&
3389
3432
  fetcher.state !== "idle" &&
3390
- fetcher.data === undefined &&
3391
- // If a fetcher.load redirected then it'll be "loading" without any data
3392
- // so ensure we're not processing the redirect from this fetcher
3393
- !fetchRedirectIds.has(key);
3394
- let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3395
- let shouldRevalidate =
3396
- cancelledFetcherLoads.includes(key) ||
3397
- isPerformingInitialLoad ||
3398
- shouldRevalidateLoader(fetcherMatch, {
3433
+ fetcher.data === undefined
3434
+ ) {
3435
+ // If the fetcher hasn't ever completed loading yet, then this isn't a
3436
+ // revalidation, it would just be a brand new load if an explicit
3437
+ // revalidation is required
3438
+ shouldRevalidate = isRevalidationRequired;
3439
+ } else {
3440
+ // Otherwise fall back on any user-defined shouldRevalidate, defaulting
3441
+ // to explicit revalidations only
3442
+ shouldRevalidate = shouldRevalidateLoader(fetcherMatch, {
3399
3443
  currentUrl,
3400
3444
  currentParams: state.matches[state.matches.length - 1].params,
3401
3445
  nextUrl,
@@ -3404,6 +3448,7 @@ function getMatchesToLoad(
3404
3448
  actionResult,
3405
3449
  defaultShouldRevalidate: isRevalidationRequired,
3406
3450
  });
3451
+ }
3407
3452
 
3408
3453
  if (shouldRevalidate) {
3409
3454
  revalidatingFetchers.push({
@@ -4090,11 +4135,13 @@ function getInternalRouterError(
4090
4135
  }
4091
4136
 
4092
4137
  // Find any returned redirect errors, starting from the lowest match
4093
- function findRedirect(results: DataResult[]): RedirectResult | undefined {
4138
+ function findRedirect(
4139
+ results: DataResult[]
4140
+ ): { result: RedirectResult; idx: number } | undefined {
4094
4141
  for (let i = results.length - 1; i >= 0; i--) {
4095
4142
  let result = results[i];
4096
4143
  if (isRedirectResult(result)) {
4097
- return result;
4144
+ return { result, idx: i };
4098
4145
  }
4099
4146
  }
4100
4147
  }
package/utils.ts CHANGED
@@ -239,12 +239,19 @@ export const immutableRouteKeys = new Set<ImmutableRouteKey>([
239
239
  "children",
240
240
  ]);
241
241
 
242
+ type RequireOne<T, Key = keyof T> = Exclude<
243
+ {
244
+ [K in keyof T]: K extends Key ? Omit<T, K> & Required<Pick<T, K>> : never;
245
+ }[keyof T],
246
+ undefined
247
+ >;
248
+
242
249
  /**
243
250
  * lazy() function to load a route definition, which can add non-matching
244
251
  * related properties to a route
245
252
  */
246
253
  export interface LazyRouteFunction<R extends AgnosticRouteObject> {
247
- (): Promise<Omit<R, ImmutableRouteKey>>;
254
+ (): Promise<RequireOne<Omit<R, ImmutableRouteKey>>>;
248
255
  }
249
256
 
250
257
  /**
@@ -1310,7 +1317,7 @@ export class DeferredData {
1310
1317
  // We store a little wrapper promise that will be extended with
1311
1318
  // _data/_error props upon resolve/reject
1312
1319
  let promise: TrackedPromise = Promise.race([value, this.abortPromise]).then(
1313
- (data) => this.onSettle(promise, key, null, data as unknown),
1320
+ (data) => this.onSettle(promise, key, undefined, data as unknown),
1314
1321
  (error) => this.onSettle(promise, key, error as unknown)
1315
1322
  );
1316
1323
 
@@ -1344,7 +1351,19 @@ export class DeferredData {
1344
1351
  this.unlistenAbortSignal();
1345
1352
  }
1346
1353
 
1347
- if (error) {
1354
+ // If the promise was resolved/rejected with undefined, we'll throw an error as you
1355
+ // should always resolve with a value or null
1356
+ if (error === undefined && data === undefined) {
1357
+ let undefinedError = new Error(
1358
+ `Deferred data for key "${key}" resolved/rejected with \`undefined\`, ` +
1359
+ `you must resolve/reject with a value or \`null\`.`
1360
+ );
1361
+ Object.defineProperty(promise, "_error", { get: () => undefinedError });
1362
+ this.emit(false, key);
1363
+ return Promise.reject(undefinedError);
1364
+ }
1365
+
1366
+ if (data === undefined) {
1348
1367
  Object.defineProperty(promise, "_error", { get: () => error });
1349
1368
  this.emit(false, key);
1350
1369
  return Promise.reject(error);