@remix-run/router 1.1.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # `@remix-run/router`
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Remove `unstable_` prefix from `createStaticHandler`/`createStaticRouter`/`StaticRouterProvider` ([#9738](https://github.com/remix-run/react-router/pull/9738))
8
+
9
+ ### Patch Changes
10
+
11
+ - Fix explicit `replace` on submissions and `PUSH` on submission to new paths ([#9734](https://github.com/remix-run/react-router/pull/9734))
12
+ - Fix a few bugs where loader/action data wasn't properly cleared on errors ([#9735](https://github.com/remix-run/react-router/pull/9735))
13
+ - Prevent `useLoaderData` usage in `errorElement` ([#9735](https://github.com/remix-run/react-router/pull/9735))
14
+ - Skip initial scroll restoration for SSR apps with `hydrationData` ([#9664](https://github.com/remix-run/react-router/pull/9664))
15
+
3
16
  ## 1.1.0
4
17
 
5
18
  This release introduces support for [Optional Route Segments](https://github.com/remix-run/react-router/issues/9546). Now, adding a `?` to the end of any path segment will make that entire segment optional. This works for both static segments and dynamic parameters.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.1.0
2
+ * @remix-run/router v1.2.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1415,8 +1415,10 @@ function createRouter(init) {
1415
1415
  // we don't get the saved positions from <ScrollRestoration /> until _after_
1416
1416
  // the initial render, we need to manually trigger a separate updateState to
1417
1417
  // send along the restoreScrollPosition
1418
+ // Set to true if we have `hydrationData` since we assume we were SSR'd and that
1419
+ // SSR did the initial scroll restoration.
1418
1420
 
1419
- let initialScrollRestored = false;
1421
+ let initialScrollRestored = init.hydrationData != null;
1420
1422
  let initialMatches = matchRoutes(dataRoutes, init.history.location, init.basename);
1421
1423
  let initialErrors = null;
1422
1424
 
@@ -1444,7 +1446,8 @@ function createRouter(init) {
1444
1446
  matches: initialMatches,
1445
1447
  initialized,
1446
1448
  navigation: IDLE_NAVIGATION,
1447
- restoreScrollPosition: null,
1449
+ // Don't restore on initial updateState() if we were SSR'd
1450
+ restoreScrollPosition: init.hydrationData != null ? false : null,
1448
1451
  preventScrollReset: false,
1449
1452
  revalidation: "idle",
1450
1453
  loaderData: init.hydrationData && init.hydrationData.loaderData || {},
@@ -1553,14 +1556,30 @@ function createRouter(init) {
1553
1556
  // location, indicating we redirected from the action (avoids false
1554
1557
  // positives for loading/submissionRedirect when actionData returned
1555
1558
  // on a prior submission)
1556
- let isActionReload = state.actionData != null && state.navigation.formMethod != null && state.navigation.state === "loading" && ((_state$navigation$for = state.navigation.formAction) == null ? void 0 : _state$navigation$for.split("?")[0]) === location.pathname; // Always preserve any existing loaderData from re-used routes
1557
-
1558
- let newLoaderData = newState.loaderData ? {
1559
- loaderData: mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [])
1560
- } : {};
1561
- updateState(_extends({}, isActionReload ? {} : {
1562
- actionData: null
1563
- }, newState, newLoaderData, {
1559
+ let isActionReload = state.actionData != null && state.navigation.formMethod != null && state.navigation.state === "loading" && ((_state$navigation$for = state.navigation.formAction) == null ? void 0 : _state$navigation$for.split("?")[0]) === location.pathname;
1560
+ let actionData;
1561
+
1562
+ if (newState.actionData) {
1563
+ if (Object.keys(newState.actionData).length > 0) {
1564
+ actionData = newState.actionData;
1565
+ } else {
1566
+ // Empty actionData -> clear prior actionData due to an action error
1567
+ actionData = null;
1568
+ }
1569
+ } else if (isActionReload) {
1570
+ // Keep the current data if we're wrapping up the action reload
1571
+ actionData = state.actionData;
1572
+ } else {
1573
+ // Clear actionData on any other completed navigations
1574
+ actionData = null;
1575
+ } // Always preserve any existing loaderData from re-used routes
1576
+
1577
+
1578
+ let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData;
1579
+ updateState(_extends({}, newState, {
1580
+ // matches, errors, fetchers go through as-is
1581
+ actionData,
1582
+ loaderData,
1564
1583
  historyAction: pendingAction,
1565
1584
  location,
1566
1585
  initialized: true,
@@ -1606,7 +1625,19 @@ function createRouter(init) {
1606
1625
  // without having to touch history
1607
1626
 
1608
1627
  location = _extends({}, location, init.history.encodeLocation(location));
1609
- let historyAction = (opts && opts.replace) === true || submission != null && isMutationMethod(submission.formMethod) ? exports.Action.Replace : exports.Action.Push;
1628
+ let userReplace = opts && opts.replace != null ? opts.replace : undefined;
1629
+ let historyAction = exports.Action.Push;
1630
+
1631
+ if (userReplace === true) {
1632
+ historyAction = exports.Action.Replace;
1633
+ } else if (userReplace === false) ; else if (submission != null && isMutationMethod(submission.formMethod) && submission.formAction === state.location.pathname + state.location.search) {
1634
+ // By default on submissions to the current location we REPLACE so that
1635
+ // users don't have to double-click the back button to get to the prior
1636
+ // location. If the user redirects to a different location from the
1637
+ // action/loader this will be ignored and the redirect will be a PUSH
1638
+ historyAction = exports.Action.Replace;
1639
+ }
1640
+
1610
1641
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1611
1642
  return await startNavigation(historyAction, location, {
1612
1643
  submission,
@@ -1750,11 +1781,14 @@ function createRouter(init) {
1750
1781
 
1751
1782
 
1752
1783
  pendingNavigationController = null;
1753
- completeNavigation(location, {
1754
- matches,
1784
+ completeNavigation(location, _extends({
1785
+ matches
1786
+ }, pendingActionData ? {
1787
+ actionData: pendingActionData
1788
+ } : {}, {
1755
1789
  loaderData,
1756
1790
  errors
1757
- });
1791
+ }));
1758
1792
  } // Call the action matched by the leaf route for this navigation and handle
1759
1793
  // redirects/errors
1760
1794
 
@@ -1794,7 +1828,18 @@ function createRouter(init) {
1794
1828
  }
1795
1829
 
1796
1830
  if (isRedirectResult(result)) {
1797
- await startRedirectNavigation(state, result, opts && opts.replace === true);
1831
+ let replace;
1832
+
1833
+ if (opts && opts.replace != null) {
1834
+ replace = opts.replace;
1835
+ } else {
1836
+ // If the user didn't explicity indicate replace behavior, replace if
1837
+ // we redirected to the exact same location we're currently at to avoid
1838
+ // double back-buttons
1839
+ replace = result.location === state.location.pathname + state.location.search;
1840
+ }
1841
+
1842
+ await startRedirectNavigation(state, result, replace);
1798
1843
  return {
1799
1844
  shortCircuited: true
1800
1845
  };
@@ -1813,6 +1858,8 @@ function createRouter(init) {
1813
1858
  }
1814
1859
 
1815
1860
  return {
1861
+ // Send back an empty object we can use to clear out any prior actionData
1862
+ pendingActionData: {},
1816
1863
  pendingActionError: {
1817
1864
  [boundaryMatch.route.id]: result.error
1818
1865
  }
@@ -1856,13 +1903,14 @@ function createRouter(init) {
1856
1903
  cancelActiveDeferreds(routeId => !(matches && matches.some(m => m.route.id === routeId)) || matchesToLoad && matchesToLoad.some(m => m.route.id === routeId)); // Short circuit if we have no loaders to run
1857
1904
 
1858
1905
  if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
1859
- completeNavigation(location, {
1906
+ completeNavigation(location, _extends({
1860
1907
  matches,
1861
- loaderData: mergeLoaderData(state.loaderData, {}, matches),
1908
+ loaderData: {},
1862
1909
  // Commit pending error if we're short circuiting
1863
- errors: pendingError || null,
1864
- actionData: pendingActionData || null
1865
- });
1910
+ errors: pendingError || null
1911
+ }, pendingActionData ? {
1912
+ actionData: pendingActionData
1913
+ } : {}));
1866
1914
  return {
1867
1915
  shortCircuited: true
1868
1916
  };
@@ -1882,14 +1930,19 @@ function createRouter(init) {
1882
1930
  formMethod: undefined,
1883
1931
  formAction: undefined,
1884
1932
  formEncType: undefined,
1885
- formData: undefined
1933
+ formData: undefined,
1934
+ " _hasFetcherDoneAnything ": true
1886
1935
  };
1887
1936
  state.fetchers.set(key, revalidatingFetcher);
1888
1937
  });
1938
+ let actionData = pendingActionData || state.actionData;
1889
1939
  updateState(_extends({
1890
- navigation: loadingNavigation,
1891
- actionData: pendingActionData || state.actionData || null
1892
- }, revalidatingFetchers.length > 0 ? {
1940
+ navigation: loadingNavigation
1941
+ }, actionData ? Object.keys(actionData).length === 0 ? {
1942
+ actionData: null
1943
+ } : {
1944
+ actionData
1945
+ } : {}, revalidatingFetchers.length > 0 ? {
1893
1946
  fetchers: new Map(state.fetchers)
1894
1947
  } : {}));
1895
1948
  }
@@ -2013,7 +2066,8 @@ function createRouter(init) {
2013
2066
  let fetcher = _extends({
2014
2067
  state: "submitting"
2015
2068
  }, submission, {
2016
- data: existingFetcher && existingFetcher.data
2069
+ data: existingFetcher && existingFetcher.data,
2070
+ " _hasFetcherDoneAnything ": true
2017
2071
  });
2018
2072
 
2019
2073
  state.fetchers.set(key, fetcher);
@@ -2043,14 +2097,15 @@ function createRouter(init) {
2043
2097
  let loadingFetcher = _extends({
2044
2098
  state: "loading"
2045
2099
  }, submission, {
2046
- data: undefined
2100
+ data: undefined,
2101
+ " _hasFetcherDoneAnything ": true
2047
2102
  });
2048
2103
 
2049
2104
  state.fetchers.set(key, loadingFetcher);
2050
2105
  updateState({
2051
2106
  fetchers: new Map(state.fetchers)
2052
2107
  });
2053
- return startRedirectNavigation(state, actionResult);
2108
+ return startRedirectNavigation(state, actionResult, false, true);
2054
2109
  } // Process any non-redirect errors thrown
2055
2110
 
2056
2111
 
@@ -2075,7 +2130,9 @@ function createRouter(init) {
2075
2130
  let loadFetcher = _extends({
2076
2131
  state: "loading",
2077
2132
  data: actionResult.data
2078
- }, submission);
2133
+ }, submission, {
2134
+ " _hasFetcherDoneAnything ": true
2135
+ });
2079
2136
 
2080
2137
  state.fetchers.set(key, loadFetcher);
2081
2138
  let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, {
@@ -2097,7 +2154,8 @@ function createRouter(init) {
2097
2154
  formMethod: undefined,
2098
2155
  formAction: undefined,
2099
2156
  formEncType: undefined,
2100
- formData: undefined
2157
+ formData: undefined,
2158
+ " _hasFetcherDoneAnything ": true
2101
2159
  };
2102
2160
  state.fetchers.set(staleKey, revalidatingFetcher);
2103
2161
  fetchControllers.set(staleKey, abortController);
@@ -2138,7 +2196,8 @@ function createRouter(init) {
2138
2196
  formMethod: undefined,
2139
2197
  formAction: undefined,
2140
2198
  formEncType: undefined,
2141
- formData: undefined
2199
+ formData: undefined,
2200
+ " _hasFetcherDoneAnything ": true
2142
2201
  };
2143
2202
  state.fetchers.set(key, doneFetcher);
2144
2203
  let didAbortFetchLoads = abortStaleFetchLoads(loadId); // If we are currently in a navigation loading state and this fetcher is
@@ -2160,7 +2219,7 @@ function createRouter(init) {
2160
2219
  // manually merge here since we aren't going through completeNavigation
2161
2220
  updateState(_extends({
2162
2221
  errors,
2163
- loaderData: mergeLoaderData(state.loaderData, loaderData, matches)
2222
+ loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors)
2164
2223
  }, didAbortFetchLoads ? {
2165
2224
  fetchers: new Map(state.fetchers)
2166
2225
  } : {}));
@@ -2179,7 +2238,8 @@ function createRouter(init) {
2179
2238
  formEncType: undefined,
2180
2239
  formData: undefined
2181
2240
  }, submission, {
2182
- data: existingFetcher && existingFetcher.data
2241
+ data: existingFetcher && existingFetcher.data,
2242
+ " _hasFetcherDoneAnything ": true
2183
2243
  });
2184
2244
 
2185
2245
  state.fetchers.set(key, loadingFetcher);
@@ -2239,7 +2299,8 @@ function createRouter(init) {
2239
2299
  formMethod: undefined,
2240
2300
  formAction: undefined,
2241
2301
  formEncType: undefined,
2242
- formData: undefined
2302
+ formData: undefined,
2303
+ " _hasFetcherDoneAnything ": true
2243
2304
  };
2244
2305
  state.fetchers.set(key, doneFetcher);
2245
2306
  updateState({
@@ -2267,14 +2328,19 @@ function createRouter(init) {
2267
2328
  */
2268
2329
 
2269
2330
 
2270
- async function startRedirectNavigation(state, redirect, replace) {
2331
+ async function startRedirectNavigation(state, redirect, replace, isFetchActionRedirect) {
2271
2332
  var _window;
2272
2333
 
2273
2334
  if (redirect.revalidate) {
2274
2335
  isRevalidationRequired = true;
2275
2336
  }
2276
2337
 
2277
- let redirectLocation = createLocation(state.location, redirect.location);
2338
+ let redirectLocation = createLocation(state.location, redirect.location, // TODO: This can be removed once we get rid of useTransition in Remix v2
2339
+ _extends({
2340
+ _isRedirect: true
2341
+ }, isFetchActionRedirect ? {
2342
+ _isFetchActionRedirect: true
2343
+ } : {}));
2278
2344
  invariant(redirectLocation, "Expected a location on the redirect navigation"); // Check if this an external redirect that goes to a new origin
2279
2345
 
2280
2346
  if (typeof ((_window = window) == null ? void 0 : _window.location) !== "undefined") {
@@ -2400,7 +2466,8 @@ function createRouter(init) {
2400
2466
  formMethod: undefined,
2401
2467
  formAction: undefined,
2402
2468
  formEncType: undefined,
2403
- formData: undefined
2469
+ formData: undefined,
2470
+ " _hasFetcherDoneAnything ": true
2404
2471
  };
2405
2472
  state.fetchers.set(key, doneFetcher);
2406
2473
  }
@@ -2543,8 +2610,8 @@ function createRouter(init) {
2543
2610
  //#region createStaticHandler
2544
2611
  ////////////////////////////////////////////////////////////////////////////////
2545
2612
 
2546
- function unstable_createStaticHandler(routes, opts) {
2547
- invariant(routes.length > 0, "You must provide a non-empty routes array to unstable_createStaticHandler");
2613
+ function createStaticHandler(routes, opts) {
2614
+ invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
2548
2615
  let dataRoutes = convertRoutesToDataRoutes(routes);
2549
2616
  let basename = (opts ? opts.basename : null) || "/";
2550
2617
  /**
@@ -2866,7 +2933,10 @@ function unstable_createStaticHandler(routes, opts) {
2866
2933
  if (matchesToLoad.length === 0) {
2867
2934
  return {
2868
2935
  matches,
2869
- loaderData: {},
2936
+ // Add a null for all matched routes for proper revalidation on the client
2937
+ loaderData: matches.reduce((acc, m) => Object.assign(acc, {
2938
+ [m.route.id]: null
2939
+ }), {}),
2870
2940
  errors: pendingActionError || null,
2871
2941
  statusCode: 200,
2872
2942
  loaderHeaders: {}
@@ -2878,17 +2948,25 @@ function unstable_createStaticHandler(routes, opts) {
2878
2948
  if (request.signal.aborted) {
2879
2949
  let method = isRouteRequest ? "queryRoute" : "query";
2880
2950
  throw new Error(method + "() call aborted");
2881
- } // Can't do anything with these without the Remix side of things, so just
2882
- // cancel them for now
2951
+ }
2883
2952
 
2953
+ let executedLoaders = new Set();
2954
+ results.forEach((result, i) => {
2955
+ executedLoaders.add(matchesToLoad[i].route.id); // Can't do anything with these without the Remix side of things, so just
2956
+ // cancel them for now
2884
2957
 
2885
- results.forEach(result => {
2886
2958
  if (isDeferredResult(result)) {
2887
2959
  result.deferredData.cancel();
2888
2960
  }
2889
2961
  }); // Process and commit output from loaders
2890
2962
 
2891
- let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError);
2963
+ let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError); // Add a null for any non-loader matches for proper revalidation on the client
2964
+
2965
+ matches.forEach(match => {
2966
+ if (!executedLoaders.has(match.route.id)) {
2967
+ context.loaderData[match.route.id] = null;
2968
+ }
2969
+ });
2892
2970
  return _extends({}, context, {
2893
2971
  matches
2894
2972
  });
@@ -3010,7 +3088,7 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3010
3088
  }
3011
3089
 
3012
3090
  function getMatchesToLoad(state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, pendingActionData, pendingError, fetchLoadMatches) {
3013
- let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : null; // Pick navigation matches that are net-new or qualify for revalidation
3091
+ let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined; // Pick navigation matches that are net-new or qualify for revalidation
3014
3092
 
3015
3093
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3016
3094
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
@@ -3180,9 +3258,10 @@ async function callLoaderOrAction(type, request, match, matches, basename, isSta
3180
3258
  }
3181
3259
 
3182
3260
  let data;
3183
- let contentType = result.headers.get("Content-Type");
3261
+ let contentType = result.headers.get("Content-Type"); // Check between word boundaries instead of startsWith() due to the last
3262
+ // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
3184
3263
 
3185
- if (contentType && contentType.startsWith("application/json")) {
3264
+ if (contentType && /\bapplication\/json\b/.test(contentType)) {
3186
3265
  data = await result.json();
3187
3266
  } else {
3188
3267
  data = await result.text();
@@ -3287,9 +3366,11 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
3287
3366
 
3288
3367
  if (errors[boundaryMatch.route.id] == null) {
3289
3368
  errors[boundaryMatch.route.id] = error;
3290
- } // Once we find our first (highest) error, we set the status code and
3291
- // prevent deeper status codes from overriding
3369
+ } // Clear our any prior loaderData for the throwing route
3370
+
3292
3371
 
3372
+ loaderData[id] = undefined; // Once we find our first (highest) error, we set the status code and
3373
+ // prevent deeper status codes from overriding
3293
3374
 
3294
3375
  if (!foundError) {
3295
3376
  foundError = true;
@@ -3315,10 +3396,12 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
3315
3396
  }
3316
3397
  }
3317
3398
  }); // If we didn't consume the pending action error (i.e., all loaders
3318
- // resolved), then consume it here
3399
+ // resolved), then consume it here. Also clear out any loaderData for the
3400
+ // throwing route
3319
3401
 
3320
3402
  if (pendingError) {
3321
3403
  errors = pendingError;
3404
+ loaderData[Object.keys(pendingError)[0]] = undefined;
3322
3405
  }
3323
3406
 
3324
3407
  return {
@@ -3365,7 +3448,8 @@ function processLoaderData(state, matches, matchesToLoad, results, pendingError,
3365
3448
  formMethod: undefined,
3366
3449
  formAction: undefined,
3367
3450
  formEncType: undefined,
3368
- formData: undefined
3451
+ formData: undefined,
3452
+ " _hasFetcherDoneAnything ": true
3369
3453
  };
3370
3454
  state.fetchers.set(key, doneFetcher);
3371
3455
  }
@@ -3377,16 +3461,26 @@ function processLoaderData(state, matches, matchesToLoad, results, pendingError,
3377
3461
  };
3378
3462
  }
3379
3463
 
3380
- function mergeLoaderData(loaderData, newLoaderData, matches) {
3464
+ function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
3381
3465
  let mergedLoaderData = _extends({}, newLoaderData);
3382
3466
 
3383
- matches.forEach(match => {
3467
+ for (let match of matches) {
3384
3468
  let id = match.route.id;
3385
3469
 
3386
- if (newLoaderData[id] === undefined && loaderData[id] !== undefined) {
3470
+ if (newLoaderData.hasOwnProperty(id)) {
3471
+ if (newLoaderData[id] !== undefined) {
3472
+ mergedLoaderData[id] = newLoaderData[id];
3473
+ }
3474
+ } else if (loaderData[id] !== undefined) {
3387
3475
  mergedLoaderData[id] = loaderData[id];
3388
3476
  }
3389
- });
3477
+
3478
+ if (errors && errors.hasOwnProperty(id)) {
3479
+ // Don't keep any loader data below the boundary
3480
+ break;
3481
+ }
3482
+ }
3483
+
3390
3484
  return mergedLoaderData;
3391
3485
  } // Find the nearest error boundary, looking upwards from the leaf route (or the
3392
3486
  // route specified by routeId) for the closest ancestor error boundary,
@@ -3608,6 +3702,7 @@ exports.createHashHistory = createHashHistory;
3608
3702
  exports.createMemoryHistory = createMemoryHistory;
3609
3703
  exports.createPath = createPath;
3610
3704
  exports.createRouter = createRouter;
3705
+ exports.createStaticHandler = createStaticHandler;
3611
3706
  exports.defer = defer;
3612
3707
  exports.generatePath = generatePath;
3613
3708
  exports.getStaticContextFromError = getStaticContextFromError;
@@ -3624,6 +3719,5 @@ exports.redirect = redirect;
3624
3719
  exports.resolvePath = resolvePath;
3625
3720
  exports.resolveTo = resolveTo;
3626
3721
  exports.stripBasename = stripBasename;
3627
- exports.unstable_createStaticHandler = unstable_createStaticHandler;
3628
3722
  exports.warning = warning;
3629
3723
  //# sourceMappingURL=router.cjs.js.map