@remix-run/router 1.7.1 → 1.7.2-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/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.0",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -1591,7 +1591,15 @@ export function createRouter(init: RouterInit): Router {
1591
1591
  // If any loaders returned a redirect Response, start a new REPLACE navigation
1592
1592
  let redirect = findRedirect(results);
1593
1593
  if (redirect) {
1594
- await startRedirectNavigation(state, redirect, { replace });
1594
+ if (redirect.idx >= matchesToLoad.length) {
1595
+ // If this redirect came from a fetcher make sure we mark it in
1596
+ // fetchRedirectIds so it doesn't get revalidated on the next set of
1597
+ // loader executions
1598
+ let fetcherKey =
1599
+ revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
1600
+ fetchRedirectIds.add(fetcherKey);
1601
+ }
1602
+ await startRedirectNavigation(state, redirect.result, { replace });
1595
1603
  return { shortCircuited: true };
1596
1604
  }
1597
1605
 
@@ -1739,6 +1747,7 @@ export function createRouter(init: RouterInit): Router {
1739
1747
  );
1740
1748
  fetchControllers.set(key, abortController);
1741
1749
 
1750
+ let originatingLoadId = incrementingLoadId;
1742
1751
  let actionResult = await callLoaderOrAction(
1743
1752
  "action",
1744
1753
  fetchRequest,
@@ -1760,15 +1769,26 @@ export function createRouter(init: RouterInit): Router {
1760
1769
 
1761
1770
  if (isRedirectResult(actionResult)) {
1762
1771
  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
- });
1772
+ if (pendingNavigationLoadId > originatingLoadId) {
1773
+ // A new navigation was kicked off after our action started, so that
1774
+ // should take precedence over this redirect navigation. We already
1775
+ // set isRevalidationRequired so all loaders for the new route should
1776
+ // fire unless opted out via shouldRevalidate
1777
+ let doneFetcher = getDoneFetcher(undefined);
1778
+ state.fetchers.set(key, doneFetcher);
1779
+ updateState({ fetchers: new Map(state.fetchers) });
1780
+ return;
1781
+ } else {
1782
+ fetchRedirectIds.add(key);
1783
+ let loadingFetcher = getLoadingFetcher(submission);
1784
+ state.fetchers.set(key, loadingFetcher);
1785
+ updateState({ fetchers: new Map(state.fetchers) });
1786
+
1787
+ return startRedirectNavigation(state, actionResult, {
1788
+ submission,
1789
+ isFetchActionRedirect: true,
1790
+ });
1791
+ }
1772
1792
  }
1773
1793
 
1774
1794
  // Process any non-redirect errors thrown
@@ -1875,7 +1895,15 @@ export function createRouter(init: RouterInit): Router {
1875
1895
 
1876
1896
  let redirect = findRedirect(results);
1877
1897
  if (redirect) {
1878
- return startRedirectNavigation(state, redirect);
1898
+ if (redirect.idx >= matchesToLoad.length) {
1899
+ // If this redirect came from a fetcher make sure we mark it in
1900
+ // fetchRedirectIds so it doesn't get revalidated on the next set of
1901
+ // loader executions
1902
+ let fetcherKey =
1903
+ revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
1904
+ fetchRedirectIds.add(fetcherKey);
1905
+ }
1906
+ return startRedirectNavigation(state, redirect.result);
1879
1907
  }
1880
1908
 
1881
1909
  // Process and commit output from loaders
@@ -1962,6 +1990,7 @@ export function createRouter(init: RouterInit): Router {
1962
1990
  );
1963
1991
  fetchControllers.set(key, abortController);
1964
1992
 
1993
+ let originatingLoadId = incrementingLoadId;
1965
1994
  let result: DataResult = await callLoaderOrAction(
1966
1995
  "loader",
1967
1996
  fetchRequest,
@@ -1994,9 +2023,18 @@ export function createRouter(init: RouterInit): Router {
1994
2023
 
1995
2024
  // If the loader threw a redirect Response, start a new REPLACE navigation
1996
2025
  if (isRedirectResult(result)) {
1997
- fetchRedirectIds.add(key);
1998
- await startRedirectNavigation(state, result);
1999
- return;
2026
+ if (pendingNavigationLoadId > originatingLoadId) {
2027
+ // A new navigation was kicked off after our loader started, so that
2028
+ // should take precedence over this redirect navigation
2029
+ let doneFetcher = getDoneFetcher(undefined);
2030
+ state.fetchers.set(key, doneFetcher);
2031
+ updateState({ fetchers: new Map(state.fetchers) });
2032
+ return;
2033
+ } else {
2034
+ fetchRedirectIds.add(key);
2035
+ await startRedirectNavigation(state, result);
2036
+ return;
2037
+ }
2000
2038
  }
2001
2039
 
2002
2040
  // Process any non-redirect errors thrown
@@ -3360,7 +3398,9 @@ function getMatchesToLoad(
3360
3398
  let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
3361
3399
 
3362
3400
  // If the fetcher path no longer matches, push it in with null matches so
3363
- // we can trigger a 404 in callLoadersAndMaybeResolveData
3401
+ // we can trigger a 404 in callLoadersAndMaybeResolveData. Note this is
3402
+ // currently only a use-case for Remix HMR where the route tree can change
3403
+ // at runtime and remove a route previously loaded via a fetcher
3364
3404
  if (!fetcherMatches) {
3365
3405
  revalidatingFetchers.push({
3366
3406
  key,
@@ -3374,28 +3414,31 @@ function getMatchesToLoad(
3374
3414
  }
3375
3415
 
3376
3416
  // 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
3417
+ // load from a static href. They revalidate based on explicit revalidation
3418
+ // (submission, useRevalidator, or X-Remix-Revalidate)
3386
3419
  let fetcher = state.fetchers.get(key);
3387
- let isPerformingInitialLoad =
3420
+ let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3421
+
3422
+ let shouldRevalidate = false;
3423
+ if (fetchRedirectIds.has(key)) {
3424
+ // Never trigger a revalidation of an actively redirecting fetcher
3425
+ shouldRevalidate = false;
3426
+ } else if (cancelledFetcherLoads.includes(key)) {
3427
+ // Always revalidate if the fetcher was cancelled
3428
+ shouldRevalidate = true;
3429
+ } else if (
3388
3430
  fetcher &&
3389
3431
  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, {
3432
+ fetcher.data === undefined
3433
+ ) {
3434
+ // If the fetcher hasn't ever completed loading yet, then this isn't a
3435
+ // revalidation, it would just be a brand new load if an explicit
3436
+ // revalidation is required
3437
+ shouldRevalidate = isRevalidationRequired;
3438
+ } else {
3439
+ // Otherwise fall back on any user-defined shouldRevalidate, defaulting
3440
+ // to explicit revalidations only
3441
+ shouldRevalidate = shouldRevalidateLoader(fetcherMatch, {
3399
3442
  currentUrl,
3400
3443
  currentParams: state.matches[state.matches.length - 1].params,
3401
3444
  nextUrl,
@@ -3404,6 +3447,7 @@ function getMatchesToLoad(
3404
3447
  actionResult,
3405
3448
  defaultShouldRevalidate: isRevalidationRequired,
3406
3449
  });
3450
+ }
3407
3451
 
3408
3452
  if (shouldRevalidate) {
3409
3453
  revalidatingFetchers.push({
@@ -4090,11 +4134,13 @@ function getInternalRouterError(
4090
4134
  }
4091
4135
 
4092
4136
  // Find any returned redirect errors, starting from the lowest match
4093
- function findRedirect(results: DataResult[]): RedirectResult | undefined {
4137
+ function findRedirect(
4138
+ results: DataResult[]
4139
+ ): { result: RedirectResult; idx: number } | undefined {
4094
4140
  for (let i = results.length - 1; i >= 0; i--) {
4095
4141
  let result = results[i];
4096
4142
  if (isRedirectResult(result)) {
4097
- return result;
4143
+ return { result, idx: i };
4098
4144
  }
4099
4145
  }
4100
4146
  }
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);