@remix-run/router 1.3.0 → 1.3.1-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
@@ -383,7 +383,7 @@ export declare class ErrorResponse {
383
383
  }
384
384
  /**
385
385
  * Check if the given error is an ErrorResponse generated from a 4xx/5xx
386
- * Response throw from an action/loader
386
+ * Response thrown from an action/loader
387
387
  */
388
- export declare function isRouteErrorResponse(e: any): e is ErrorResponse;
388
+ export declare function isRouteErrorResponse(error: any): error is ErrorResponse;
389
389
  export {};
package/history.ts CHANGED
@@ -625,14 +625,10 @@ function getUrlBasedHistory(
625
625
  } else {
626
626
  warning(
627
627
  false,
628
- // TODO: Write up a doc that explains our blocking strategy in detail
629
- // and link to it here so people can understand better what is going on
630
- // and how to avoid it.
631
- `You are trying to block a POP navigation to a location that was not ` +
632
- `created by @remix-run/router. The block will fail silently in ` +
633
- `production, but in general you should do all navigation with the ` +
634
- `router (instead of using window.history.pushState directly) ` +
635
- `to avoid this situation.`
628
+ `You are trying to perform a POP navigation to a location that was not ` +
629
+ `created by @remix-run/router. This will fail silently in production. ` +
630
+ `You should navigate via the router to avoid this situation (instead of ` +
631
+ `using window.history.pushState/window.location.hash).`
636
632
  );
637
633
  }
638
634
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.3.0",
3
+ "version": "1.3.1-pre.0",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  SuccessResult,
22
22
  AgnosticRouteMatch,
23
23
  MutationFormMethod,
24
+ ShouldRevalidateFunction,
24
25
  } from "./utils";
25
26
  import {
26
27
  DeferredData,
@@ -549,25 +550,22 @@ interface HandleLoadersResult extends ShortCircuitable {
549
550
  }
550
551
 
551
552
  /**
552
- * Tuple of [key, href, DataRouteMatch, DataRouteMatch[]] for a revalidating
553
- * fetcher.load()
553
+ * Cached info for active fetcher.load() instances so they can participate
554
+ * in revalidation
554
555
  */
555
- type RevalidatingFetcher = [
556
- string,
557
- string,
558
- AgnosticDataRouteMatch,
559
- AgnosticDataRouteMatch[]
560
- ];
556
+ interface FetchLoadMatch {
557
+ routeId: string;
558
+ path: string;
559
+ match: AgnosticDataRouteMatch;
560
+ matches: AgnosticDataRouteMatch[];
561
+ }
561
562
 
562
563
  /**
563
- * Tuple of [href, DataRouteMatch, DataRouteMatch[]] for an active
564
- * fetcher.load()
564
+ * Identified fetcher.load() calls that need to be revalidated
565
565
  */
566
- type FetchLoadMatch = [
567
- string,
568
- AgnosticDataRouteMatch,
569
- AgnosticDataRouteMatch[]
570
- ];
566
+ interface RevalidatingFetcher extends FetchLoadMatch {
567
+ key: string;
568
+ }
571
569
 
572
570
  /**
573
571
  * Wrapper object to allow us to throw any response out from callLoaderOrAction
@@ -1121,8 +1119,13 @@ export function createRouter(init: RouterInit): Router {
1121
1119
  return;
1122
1120
  }
1123
1121
 
1124
- // Short circuit if it's only a hash change
1125
- if (isHashChangeOnly(state.location, location)) {
1122
+ // Short circuit if it's only a hash change and not a mutation submission
1123
+ // For example, on /page#hash and submit a <Form method="post"> which will
1124
+ // default to a navigation to /page
1125
+ if (
1126
+ isHashChangeOnly(state.location, location) &&
1127
+ !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))
1128
+ ) {
1126
1129
  completeNavigation(location, { matches });
1127
1130
  return;
1128
1131
  }
@@ -1380,8 +1383,8 @@ export function createRouter(init: RouterInit): Router {
1380
1383
  // preserving any new action data or existing action data (in the case of
1381
1384
  // a revalidation interrupting an actionReload)
1382
1385
  if (!isUninterruptedRevalidation) {
1383
- revalidatingFetchers.forEach(([key]) => {
1384
- let fetcher = state.fetchers.get(key);
1386
+ revalidatingFetchers.forEach((rf) => {
1387
+ let fetcher = state.fetchers.get(rf.key);
1385
1388
  let revalidatingFetcher: FetcherStates["Loading"] = {
1386
1389
  state: "loading",
1387
1390
  data: fetcher && fetcher.data,
@@ -1391,7 +1394,7 @@ export function createRouter(init: RouterInit): Router {
1391
1394
  formData: undefined,
1392
1395
  " _hasFetcherDoneAnything ": true,
1393
1396
  };
1394
- state.fetchers.set(key, revalidatingFetcher);
1397
+ state.fetchers.set(rf.key, revalidatingFetcher);
1395
1398
  });
1396
1399
  let actionData = pendingActionData || state.actionData;
1397
1400
  updateState({
@@ -1408,8 +1411,8 @@ export function createRouter(init: RouterInit): Router {
1408
1411
  }
1409
1412
 
1410
1413
  pendingNavigationLoadId = ++incrementingLoadId;
1411
- revalidatingFetchers.forEach(([key]) =>
1412
- fetchControllers.set(key, pendingNavigationController!)
1414
+ revalidatingFetchers.forEach((rf) =>
1415
+ fetchControllers.set(rf.key, pendingNavigationController!)
1413
1416
  );
1414
1417
 
1415
1418
  let { results, loaderResults, fetcherResults } =
@@ -1428,7 +1431,7 @@ export function createRouter(init: RouterInit): Router {
1428
1431
  // Clean up _after_ loaders have completed. Don't clean up if we short
1429
1432
  // circuited because fetchControllers would have been aborted and
1430
1433
  // reassigned to new controllers for the next navigation
1431
- revalidatingFetchers.forEach(([key]) => fetchControllers.delete(key));
1434
+ revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
1432
1435
 
1433
1436
  // If any loaders returned a redirect Response, start a new REPLACE navigation
1434
1437
  let redirect = findRedirect(results);
@@ -1507,6 +1510,8 @@ export function createRouter(init: RouterInit): Router {
1507
1510
  let { path, submission } = normalizeNavigateOptions(href, opts, true);
1508
1511
  let match = getTargetMatch(matches, path);
1509
1512
 
1513
+ pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1514
+
1510
1515
  if (submission && isMutationMethod(submission.formMethod)) {
1511
1516
  handleFetcherAction(key, routeId, path, match, matches, submission);
1512
1517
  return;
@@ -1514,7 +1519,7 @@ export function createRouter(init: RouterInit): Router {
1514
1519
 
1515
1520
  // Store off the match so we can call it's shouldRevalidate on subsequent
1516
1521
  // revalidations
1517
- fetchLoadMatches.set(key, [path, match, matches]);
1522
+ fetchLoadMatches.set(key, { routeId, path, match, matches });
1518
1523
  handleFetcherLoader(key, routeId, path, match, matches, submission);
1519
1524
  }
1520
1525
 
@@ -1651,8 +1656,9 @@ export function createRouter(init: RouterInit): Router {
1651
1656
  // current fetcher which we want to keep in it's current loading state which
1652
1657
  // contains it's action submission info + action data
1653
1658
  revalidatingFetchers
1654
- .filter(([staleKey]) => staleKey !== key)
1655
- .forEach(([staleKey]) => {
1659
+ .filter((rf) => rf.key !== key)
1660
+ .forEach((rf) => {
1661
+ let staleKey = rf.key;
1656
1662
  let existingFetcher = state.fetchers.get(staleKey);
1657
1663
  let revalidatingFetcher: FetcherStates["Loading"] = {
1658
1664
  state: "loading",
@@ -1684,9 +1690,7 @@ export function createRouter(init: RouterInit): Router {
1684
1690
 
1685
1691
  fetchReloadIds.delete(key);
1686
1692
  fetchControllers.delete(key);
1687
- revalidatingFetchers.forEach(([staleKey]) =>
1688
- fetchControllers.delete(staleKey)
1689
- );
1693
+ revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
1690
1694
 
1691
1695
  let redirect = findRedirect(results);
1692
1696
  if (redirect) {
@@ -1980,12 +1984,12 @@ export function createRouter(init: RouterInit): Router {
1980
1984
  ...matchesToLoad.map((match) =>
1981
1985
  callLoaderOrAction("loader", request, match, matches, router.basename)
1982
1986
  ),
1983
- ...fetchersToLoad.map(([, href, match, fetchMatches]) =>
1987
+ ...fetchersToLoad.map((f) =>
1984
1988
  callLoaderOrAction(
1985
1989
  "loader",
1986
- createClientSideRequest(init.history, href, request.signal),
1987
- match,
1988
- fetchMatches,
1990
+ createClientSideRequest(init.history, f.path, request.signal),
1991
+ f.match,
1992
+ f.matches,
1989
1993
  router.basename
1990
1994
  )
1991
1995
  ),
@@ -2004,7 +2008,7 @@ export function createRouter(init: RouterInit): Router {
2004
2008
  ),
2005
2009
  resolveDeferredResults(
2006
2010
  currentMatches,
2007
- fetchersToLoad.map(([, , match]) => match),
2011
+ fetchersToLoad.map((f) => f.match),
2008
2012
  fetcherResults,
2009
2013
  request.signal,
2010
2014
  true
@@ -2845,25 +2849,14 @@ function normalizeNavigateOptions(
2845
2849
 
2846
2850
  // Flatten submission onto URLSearchParams for GET submissions
2847
2851
  let parsedPath = parsePath(path);
2848
- try {
2849
- let searchParams = convertFormDataToSearchParams(opts.formData);
2850
- // Since fetcher GET submissions only run a single loader (as opposed to
2851
- // navigation GET submissions which run all loaders), we need to preserve
2852
- // any incoming ?index params
2853
- if (
2854
- isFetcher &&
2855
- parsedPath.search &&
2856
- hasNakedIndexQuery(parsedPath.search)
2857
- ) {
2858
- searchParams.append("index", "");
2859
- }
2860
- parsedPath.search = `?${searchParams}`;
2861
- } catch (e) {
2862
- return {
2863
- path,
2864
- error: getInternalRouterError(400),
2865
- };
2852
+ let searchParams = convertFormDataToSearchParams(opts.formData);
2853
+ // Since fetcher GET submissions only run a single loader (as opposed to
2854
+ // navigation GET submissions which run all loaders), we need to preserve
2855
+ // any incoming ?index params
2856
+ if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
2857
+ searchParams.append("index", "");
2866
2858
  }
2859
+ parsedPath.search = `?${searchParams}`;
2867
2860
 
2868
2861
  return { path: createPath(parsedPath), submission };
2869
2862
  }
@@ -2903,47 +2896,81 @@ function getMatchesToLoad(
2903
2896
  ? Object.values(pendingActionData)[0]
2904
2897
  : undefined;
2905
2898
 
2899
+ let currentUrl = history.createURL(state.location);
2900
+ let nextUrl = history.createURL(location);
2901
+
2902
+ let defaultShouldRevalidate =
2903
+ // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
2904
+ isRevalidationRequired ||
2905
+ // Clicked the same link, resubmitted a GET form
2906
+ currentUrl.toString() === nextUrl.toString() ||
2907
+ // Search params affect all loaders
2908
+ currentUrl.search !== nextUrl.search;
2909
+
2906
2910
  // Pick navigation matches that are net-new or qualify for revalidation
2907
2911
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
2908
2912
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
2909
- let navigationMatches = boundaryMatches.filter(
2910
- (match, index) =>
2911
- match.route.loader != null &&
2912
- (isNewLoader(state.loaderData, state.matches[index], match) ||
2913
- // If this route had a pending deferred cancelled it must be revalidated
2914
- cancelledDeferredRoutes.some((id) => id === match.route.id) ||
2915
- shouldRevalidateLoader(
2916
- history,
2917
- state.location,
2918
- state.matches[index],
2919
- submission,
2920
- location,
2921
- match,
2922
- isRevalidationRequired,
2923
- actionResult
2924
- ))
2925
- );
2913
+
2914
+ let navigationMatches = boundaryMatches.filter((match, index) => {
2915
+ if (match.route.loader == null) {
2916
+ return false;
2917
+ }
2918
+
2919
+ // Always call the loader on new route instances and pending defer cancellations
2920
+ if (
2921
+ isNewLoader(state.loaderData, state.matches[index], match) ||
2922
+ cancelledDeferredRoutes.some((id) => id === match.route.id)
2923
+ ) {
2924
+ return true;
2925
+ }
2926
+
2927
+ // This is the default implementation for when we revalidate. If the route
2928
+ // provides it's own implementation, then we give them full control but
2929
+ // provide this value so they can leverage it if needed after they check
2930
+ // their own specific use cases
2931
+ let currentRouteMatch = state.matches[index];
2932
+ let nextRouteMatch = match;
2933
+
2934
+ return shouldRevalidateLoader(match, {
2935
+ currentUrl,
2936
+ currentParams: currentRouteMatch.params,
2937
+ nextUrl,
2938
+ nextParams: nextRouteMatch.params,
2939
+ ...submission,
2940
+ actionResult,
2941
+ defaultShouldRevalidate:
2942
+ defaultShouldRevalidate ||
2943
+ isNewRouteInstance(currentRouteMatch, nextRouteMatch),
2944
+ });
2945
+ });
2926
2946
 
2927
2947
  // Pick fetcher.loads that need to be revalidated
2928
2948
  let revalidatingFetchers: RevalidatingFetcher[] = [];
2929
2949
  fetchLoadMatches &&
2930
- fetchLoadMatches.forEach(([href, match, fetchMatches], key) => {
2931
- // This fetcher was cancelled from a prior action submission - force reload
2932
- if (cancelledFetcherLoads.includes(key)) {
2933
- revalidatingFetchers.push([key, href, match, fetchMatches]);
2934
- } else if (isRevalidationRequired) {
2935
- let shouldRevalidate = shouldRevalidateLoader(
2936
- history,
2937
- href,
2938
- match,
2939
- submission,
2940
- href,
2941
- match,
2942
- isRevalidationRequired,
2943
- actionResult
2944
- );
2950
+ fetchLoadMatches.forEach((f, key) => {
2951
+ if (!matches.some((m) => m.route.id === f.routeId)) {
2952
+ // This fetcher is not going to be present in the subsequent render so
2953
+ // there's no need to revalidate it
2954
+ return;
2955
+ } else if (cancelledFetcherLoads.includes(key)) {
2956
+ // This fetcher was cancelled from a prior action submission - force reload
2957
+ revalidatingFetchers.push({ key, ...f });
2958
+ } else {
2959
+ // Revalidating fetchers are decoupled from the route matches since they
2960
+ // hit a static href, so they _always_ check shouldRevalidate and the
2961
+ // default is strictly if a revalidation is explicitly required (action
2962
+ // submissions, useRevalidator, X-Remix-Revalidate).
2963
+ let shouldRevalidate = shouldRevalidateLoader(f.match, {
2964
+ currentUrl,
2965
+ currentParams: state.matches[state.matches.length - 1].params,
2966
+ nextUrl,
2967
+ nextParams: matches[matches.length - 1].params,
2968
+ ...submission,
2969
+ actionResult,
2970
+ defaultShouldRevalidate,
2971
+ });
2945
2972
  if (shouldRevalidate) {
2946
- revalidatingFetchers.push([key, href, match, fetchMatches]);
2973
+ revalidatingFetchers.push({ key, ...f });
2947
2974
  }
2948
2975
  }
2949
2976
  });
@@ -2980,58 +3007,24 @@ function isNewRouteInstance(
2980
3007
  currentMatch.pathname !== match.pathname ||
2981
3008
  // splat param changed, which is not present in match.path
2982
3009
  // e.g. /files/images/avatar.jpg -> files/finances.xls
2983
- (currentPath &&
3010
+ (currentPath != null &&
2984
3011
  currentPath.endsWith("*") &&
2985
3012
  currentMatch.params["*"] !== match.params["*"])
2986
3013
  );
2987
3014
  }
2988
3015
 
2989
3016
  function shouldRevalidateLoader(
2990
- history: History,
2991
- currentLocation: string | Location,
2992
- currentMatch: AgnosticDataRouteMatch,
2993
- submission: Submission | undefined,
2994
- location: string | Location,
2995
- match: AgnosticDataRouteMatch,
2996
- isRevalidationRequired: boolean,
2997
- actionResult: DataResult | undefined
3017
+ loaderMatch: AgnosticDataRouteMatch,
3018
+ arg: Parameters<ShouldRevalidateFunction>[0]
2998
3019
  ) {
2999
- let currentUrl = history.createURL(currentLocation);
3000
- let currentParams = currentMatch.params;
3001
- let nextUrl = history.createURL(location);
3002
- let nextParams = match.params;
3003
-
3004
- // This is the default implementation as to when we revalidate. If the route
3005
- // provides it's own implementation, then we give them full control but
3006
- // provide this value so they can leverage it if needed after they check
3007
- // their own specific use cases
3008
- // Note that fetchers always provide the same current/next locations so the
3009
- // URL-based checks here don't apply to fetcher shouldRevalidate calls
3010
- let defaultShouldRevalidate =
3011
- isNewRouteInstance(currentMatch, match) ||
3012
- // Clicked the same link, resubmitted a GET form
3013
- currentUrl.toString() === nextUrl.toString() ||
3014
- // Search params affect all loaders
3015
- currentUrl.search !== nextUrl.search ||
3016
- // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
3017
- isRevalidationRequired;
3018
-
3019
- if (match.route.shouldRevalidate) {
3020
- let routeChoice = match.route.shouldRevalidate({
3021
- currentUrl,
3022
- currentParams,
3023
- nextUrl,
3024
- nextParams,
3025
- ...submission,
3026
- actionResult,
3027
- defaultShouldRevalidate,
3028
- });
3020
+ if (loaderMatch.route.shouldRevalidate) {
3021
+ let routeChoice = loaderMatch.route.shouldRevalidate(arg);
3029
3022
  if (typeof routeChoice === "boolean") {
3030
3023
  return routeChoice;
3031
3024
  }
3032
3025
  }
3033
3026
 
3034
- return defaultShouldRevalidate;
3027
+ return arg.defaultShouldRevalidate;
3035
3028
  }
3036
3029
 
3037
3030
  async function callLoaderOrAction(
@@ -3222,12 +3215,8 @@ function convertFormDataToSearchParams(formData: FormData): URLSearchParams {
3222
3215
  let searchParams = new URLSearchParams();
3223
3216
 
3224
3217
  for (let [key, value] of formData.entries()) {
3225
- invariant(
3226
- typeof value === "string",
3227
- 'File inputs are not supported with encType "application/x-www-form-urlencoded", ' +
3228
- 'please use "multipart/form-data" instead.'
3229
- );
3230
- searchParams.append(key, value);
3218
+ // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
3219
+ searchParams.append(key, value instanceof File ? value.name : value);
3231
3220
  }
3232
3221
 
3233
3222
  return searchParams;
@@ -3355,7 +3344,7 @@ function processLoaderData(
3355
3344
 
3356
3345
  // Process results from our revalidating fetchers
3357
3346
  for (let index = 0; index < revalidatingFetchers.length; index++) {
3358
- let [key, , match] = revalidatingFetchers[index];
3347
+ let { key, match } = revalidatingFetchers[index];
3359
3348
  invariant(
3360
3349
  fetcherResults !== undefined && fetcherResults[index] !== undefined,
3361
3350
  "Did not find corresponding fetcher result"
@@ -3490,8 +3479,6 @@ function getInternalRouterError(
3490
3479
  `so there is no way to handle the request.`;
3491
3480
  } else if (type === "defer-action") {
3492
3481
  errorMessage = "defer() is not supported in actions";
3493
- } else {
3494
- errorMessage = "Cannot submit binary form data using GET";
3495
3482
  }
3496
3483
  } else if (status === 403) {
3497
3484
  statusText = "Forbidden";
package/utils.ts CHANGED
@@ -1192,6 +1192,11 @@ export class DeferredData {
1192
1192
  {}
1193
1193
  );
1194
1194
 
1195
+ if (this.done) {
1196
+ // All incoming values were resolved
1197
+ this.unlistenAbortSignal();
1198
+ }
1199
+
1195
1200
  this.init = responseInit;
1196
1201
  }
1197
1202
 
@@ -1395,8 +1400,14 @@ export class ErrorResponse {
1395
1400
 
1396
1401
  /**
1397
1402
  * Check if the given error is an ErrorResponse generated from a 4xx/5xx
1398
- * Response throw from an action/loader
1403
+ * Response thrown from an action/loader
1399
1404
  */
1400
- export function isRouteErrorResponse(e: any): e is ErrorResponse {
1401
- return e instanceof ErrorResponse;
1405
+ export function isRouteErrorResponse(error: any): error is ErrorResponse {
1406
+ return (
1407
+ error != null &&
1408
+ typeof error.status === "number" &&
1409
+ typeof error.statusText === "string" &&
1410
+ typeof error.internal === "boolean" &&
1411
+ "data" in error
1412
+ );
1402
1413
  }