@remix-run/router 1.13.1 → 1.14.0-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/CHANGELOG.md CHANGED
@@ -1,5 +1,186 @@
1
1
  # `@remix-run/router`
2
2
 
3
+ ## 1.14.0-pre.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added a new `future.v7_partialHydration` future flag that enables partial hydration of a data router when Server-Side Rendering. This allows you to provide `hydrationData.loaderData` that has values for _some_ initially matched route loaders, but not all. When this flag is enabled, the router will call `loader` functions for routes that do not have hydration loader data during `router.initialize()`, and it will render down to the deepest provided `HydrateFallback` (up to the first route without hydration data) while it executes the unhydrated routes. ([#11033](https://github.com/remix-run/react-router/pull/11033))
8
+
9
+ For example, the following router has a `root` and `index` route, but only provided `hydrationData.loaderData` for the `root` route. Because the `index` route has a `loader`, we need to run that during initialization. With `future.v7_partialHydration` specified, `<RouterProvider>` will render the `RootComponent` (because it has data) and then the `IndexFallback` (since it does not have data). Once `indexLoader` finishes, application will update and display `IndexComponent`.
10
+
11
+ ```jsx
12
+ let router = createBrowserRouter(
13
+ [
14
+ {
15
+ id: "root",
16
+ path: "/",
17
+ loader: rootLoader,
18
+ Component: RootComponent,
19
+ Fallback: RootFallback,
20
+ children: [
21
+ {
22
+ id: "index",
23
+ index: true,
24
+ loader: indexLoader,
25
+ Component: IndexComponent,
26
+ HydrateFallback: IndexFallback,
27
+ },
28
+ ],
29
+ },
30
+ ],
31
+ {
32
+ future: {
33
+ v7_partialHydration: true,
34
+ },
35
+ hydrationData: {
36
+ loaderData: {
37
+ root: { message: "Hydrated from Root!" },
38
+ },
39
+ },
40
+ }
41
+ );
42
+ ```
43
+
44
+ If the above example did not have an `IndexFallback`, then `RouterProvider` would instead render the `RootFallback` while it executed the `indexLoader`.
45
+
46
+ **Note:** When `future.v7_partialHydration` is provided, the `<RouterProvider fallbackElement>` prop is ignored since you can move it to a `Fallback` on your top-most route. The `fallbackElement` prop will be removed in React Router v7 when `v7_partialHydration` behavior becomes the standard behavior.
47
+
48
+ - Add a new `future.v7_relativeSplatPath` flag to implenent a breaking bug fix to relative routing when inside a splat route. ([#11087](https://github.com/remix-run/react-router/pull/11087))
49
+
50
+ This fix was originally added in [#10983](https://github.com/remix-run/react-router/issues/10983) and was later reverted in [#11078](https://github.com/remix-run/react-router/issues/110788) because it was determined that a large number of existing applications were relying on the buggy behavior (see [#11052](https://github.com/remix-run/react-router/issues/11052))
51
+
52
+ **The Bug**
53
+ The buggy behavior is that without this flag, the default behavior when resolving relative paths is to _ignore_ any splat (`*`) portion of the current route path.
54
+
55
+ **The Background**
56
+ This decision was originally made thinking that it would make the concept of nested different sections of your apps in `<Routes>` easier if relative routing would _replace_ the current splat:
57
+
58
+ ```jsx
59
+ <BrowserRouter>
60
+ <Routes>
61
+ <Route path="/" element={<Home />} />
62
+ <Route path="dashboard/*" element={<Dashboard />} />
63
+ </Routes>
64
+ </BrowserRouter>
65
+ ```
66
+
67
+ Any paths like `/dashboard`, `/dashboard/team`, `/dashboard/projects` will match the `Dashboard` route. The dashboard component itself can then render nested `<Routes>`:
68
+
69
+ ```jsx
70
+ function Dashboard() {
71
+ return (
72
+ <div>
73
+ <h2>Dashboard</h2>
74
+ <nav>
75
+ <Link to="/">Dashboard Home</Link>
76
+ <Link to="team">Team</Link>
77
+ <Link to="projects">Projects</Link>
78
+ </nav>
79
+
80
+ <Routes>
81
+ <Route path="/" element={<DashboardHome />} />
82
+ <Route path="team" element={<DashboardTeam />} />
83
+ <Route path="projects" element={<DashboardProjects />} />
84
+ </Routes>
85
+ </div>
86
+ );
87
+ }
88
+ ```
89
+
90
+ Now, all links and route paths are relative to the router above them. This makes code splitting and compartmentalizing your app really easy. You could render the `Dashboard` as its own independent app, or embed it into your large app without making any changes to it.
91
+
92
+ **The Problem**
93
+
94
+ The problem is that this concept of ignoring part of a pth breaks a lot of other assumptions in React Router - namely that `"."` always means the current location pathname for that route. When we ignore the splat portion, we start getting invalid paths when using `"."`:
95
+
96
+ ```jsx
97
+ // If we are on URL /dashboard/team, and we want to link to /dashboard/team:
98
+ function DashboardTeam() {
99
+ // ❌ This is broken and results in <a href="/dashboard">
100
+ return <Link to=".">A broken link to the Current URL</Link>;
101
+
102
+ // ✅ This is fixed but super unintuitive since we're already at /dashboard/team!
103
+ return <Link to="./team">A broken link to the Current URL</Link>;
104
+ }
105
+ ```
106
+
107
+ We've also introduced an issue that we can no longer move our `DashboardTeam` component around our route hierarchy easily - since it behaves differently if we're underneath a non-splat route, such as `/dashboard/:widget`. Now, our `"."` links will, properly point to ourself _inclusive of the dynamic param value_ so behavior will break from it's corresponding usage in a `/dashboard/*` route.
108
+
109
+ Even worse, consider a nested splat route configuration:
110
+
111
+ ```jsx
112
+ <BrowserRouter>
113
+ <Routes>
114
+ <Route path="dashboard">
115
+ <Route path="*" element={<Dashboard />} />
116
+ </Route>
117
+ </Routes>
118
+ </BrowserRouter>
119
+ ```
120
+
121
+ Now, a `<Link to=".">` and a `<Link to="..">` inside the `Dashboard` component go to the same place! That is definitely not correct!
122
+
123
+ Another common issue arose in Data Routers (and Remix) where any `<Form>` should post to it's own route `action` if you the user doesn't specify a form action:
124
+
125
+ ```jsx
126
+ let router = createBrowserRouter({
127
+ path: "/dashboard",
128
+ children: [
129
+ {
130
+ path: "*",
131
+ action: dashboardAction,
132
+ Component() {
133
+ // ❌ This form is broken! It throws a 405 error when it submits because
134
+ // it tries to submit to /dashboard (without the splat value) and the parent
135
+ // `/dashboard` route doesn't have an action
136
+ return <Form method="post">...</Form>;
137
+ },
138
+ },
139
+ ],
140
+ });
141
+ ```
142
+
143
+ This is just a compounded issue from the above because the default location for a `Form` to submit to is itself (`"."`) - and if we ignore the splat portion, that now resolves to the parent route.
144
+
145
+ **The Solution**
146
+ If you are leveraging this behavior, it's recommended to enable the future flag, move your splat to it's own route, and leverage `../` for any links to "sibling" pages:
147
+
148
+ ```jsx
149
+ <BrowserRouter>
150
+ <Routes>
151
+ <Route path="dashboard">
152
+ <Route path="*" element={<Dashboard />} />
153
+ </Route>
154
+ </Routes>
155
+ </BrowserRouter>
156
+
157
+ function Dashboard() {
158
+ return (
159
+ <div>
160
+ <h2>Dashboard</h2>
161
+ <nav>
162
+ <Link to="..">Dashboard Home</Link>
163
+ <Link to="../team">Team</Link>
164
+ <Link to="../projects">Projects</Link>
165
+ </nav>
166
+
167
+ <Routes>
168
+ <Route path="/" element={<DashboardHome />} />
169
+ <Route path="team" element={<DashboardTeam />} />
170
+ <Route path="projects" element={<DashboardProjects />} />
171
+ </Router>
172
+ </div>
173
+ );
174
+ }
175
+ ```
176
+
177
+ This way, `.` means "the full current pathname for my route" in all cases (including static, dynamic, and splat routes) and `..` always means "my parents pathname".
178
+
179
+ ### Patch Changes
180
+
181
+ - Catch and bubble errors thrown when trying to unwrap responses from `loader`/`action` functions ([#11061](https://github.com/remix-run/react-router/pull/11061))
182
+ - Fix `relative="path"` issue when rendering `Link`/`NavLink` outside of matched routes ([#11062](https://github.com/remix-run/react-router/pull/11062))
183
+
3
184
  ## 1.13.1
4
185
 
5
186
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -5,5 +5,5 @@ export { Action, createBrowserHistory, createHashHistory, createMemoryHistory, c
5
5
  export * from "./router";
6
6
  /** @internal */
7
7
  export type { RouteManifest as UNSAFE_RouteManifest } from "./utils";
8
- export { DeferredData as UNSAFE_DeferredData, ErrorResponseImpl as UNSAFE_ErrorResponseImpl, convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes, convertRouteMatchToUiMatch as UNSAFE_convertRouteMatchToUiMatch, getPathContributingMatches as UNSAFE_getPathContributingMatches, } from "./utils";
8
+ export { DeferredData as UNSAFE_DeferredData, ErrorResponseImpl as UNSAFE_ErrorResponseImpl, convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes, convertRouteMatchToUiMatch as UNSAFE_convertRouteMatchToUiMatch, getResolveToMatches as UNSAFE_getResolveToMatches, } from "./utils";
9
9
  export { invariant as UNSAFE_invariant, warning as UNSAFE_warning, } from "./history";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.13.1
2
+ * @remix-run/router v1.14.0-pre.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1192,6 +1192,20 @@ function getPathContributingMatches(matches) {
1192
1192
  return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
1193
1193
  }
1194
1194
 
1195
+ // Return the array of pathnames for the current route matches - used to
1196
+ // generate the routePathnames input for resolveTo()
1197
+ function getResolveToMatches(matches, v7_relativeSplatPath) {
1198
+ let pathMatches = getPathContributingMatches(matches);
1199
+
1200
+ // When v7_relativeSplatPath is enabled, use the full pathname for the leaf
1201
+ // match so we include splat values for "." links. See:
1202
+ // https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329
1203
+ if (v7_relativeSplatPath) {
1204
+ return pathMatches.map((match, idx) => idx === matches.length - 1 ? match.pathname : match.pathnameBase);
1205
+ }
1206
+ return pathMatches.map(match => match.pathnameBase);
1207
+ }
1208
+
1195
1209
  /**
1196
1210
  * @private
1197
1211
  */
@@ -1224,7 +1238,7 @@ function resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {
1224
1238
  if (toPathname == null) {
1225
1239
  from = locationPathname;
1226
1240
  } else if (isPathRelative) {
1227
- let fromSegments = routePathnames[routePathnames.length - 1].replace(/^\//, "").split("/");
1241
+ let fromSegments = routePathnames.length === 0 ? [] : routePathnames[routePathnames.length - 1].replace(/^\//, "").split("/");
1228
1242
  if (toPathname.startsWith("..")) {
1229
1243
  let toSegments = toPathname.split("/");
1230
1244
 
@@ -1679,7 +1693,9 @@ function createRouter(init) {
1679
1693
  let future = _extends({
1680
1694
  v7_fetcherPersist: false,
1681
1695
  v7_normalizeFormMethod: false,
1682
- v7_prependBasename: false
1696
+ v7_partialHydration: false,
1697
+ v7_prependBasename: false,
1698
+ v7_relativeSplatPath: false
1683
1699
  }, init.future);
1684
1700
  // Cleanup function for history
1685
1701
  let unlistenHistory = null;
@@ -1715,7 +1731,13 @@ function createRouter(init) {
1715
1731
  [route.id]: error
1716
1732
  };
1717
1733
  }
1718
- let initialized =
1734
+
1735
+ // "Initialized" here really means "Can `RouterProvider` render my route tree?"
1736
+ // Prior to `route.HydrateFallback`, we only had a root `fallbackElement` so we used
1737
+ // `state.initialized` to render that instead of `<DataRoutes>`. Now that we
1738
+ // support route level fallbacks we can always render and we'll just render
1739
+ // as deep as we have data for and detect the nearest ancestor HydrateFallback
1740
+ let initialized = future.v7_partialHydration ||
1719
1741
  // All initialMatches need to be loaded before we're ready. If we have lazy
1720
1742
  // functions around still then we'll need to run them in initialize()
1721
1743
  !initialMatches.some(m => m.route.lazy) && (
@@ -1887,8 +1909,10 @@ function createRouter(init) {
1887
1909
  // in the normal navigation flow. For SSR it's expected that lazy modules are
1888
1910
  // resolved prior to router creation since we can't go into a fallbackElement
1889
1911
  // UI for SSR'd apps
1890
- if (!state.initialized) {
1891
- startNavigation(Action.Pop, state.location);
1912
+ if (!state.initialized || future.v7_partialHydration && state.matches.some(m => isUnhydratedRoute(state, m.route))) {
1913
+ startNavigation(Action.Pop, state.location, {
1914
+ initialHydration: true
1915
+ });
1892
1916
  }
1893
1917
  return router;
1894
1918
  }
@@ -2077,7 +2101,7 @@ function createRouter(init) {
2077
2101
  init.history.go(to);
2078
2102
  return;
2079
2103
  }
2080
- let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);
2104
+ let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, future.v7_relativeSplatPath, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);
2081
2105
  let {
2082
2106
  path,
2083
2107
  submission,
@@ -2278,7 +2302,7 @@ function createRouter(init) {
2278
2302
  shortCircuited,
2279
2303
  loaderData,
2280
2304
  errors
2281
- } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, flushSync, pendingActionData, pendingError);
2305
+ } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionData, pendingError);
2282
2306
  if (shortCircuited) {
2283
2307
  return;
2284
2308
  }
@@ -2326,7 +2350,7 @@ function createRouter(init) {
2326
2350
  })
2327
2351
  };
2328
2352
  } else {
2329
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename);
2353
+ result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2330
2354
  if (request.signal.aborted) {
2331
2355
  return {
2332
2356
  shortCircuited: true
@@ -2385,7 +2409,7 @@ function createRouter(init) {
2385
2409
 
2386
2410
  // Call all applicable loaders for the given matches, handling redirects,
2387
2411
  // errors, etc.
2388
- async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, flushSync, pendingActionData, pendingError) {
2412
+ async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionData, pendingError) {
2389
2413
  // Figure out the right navigation we want to use for data loading
2390
2414
  let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
2391
2415
 
@@ -2393,7 +2417,7 @@ function createRouter(init) {
2393
2417
  // we have it on the loading navigation so use that if available
2394
2418
  let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
2395
2419
  let routesToUse = inFlightDataRoutes || dataRoutes;
2396
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError);
2420
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, future.v7_partialHydration && initialHydration === true, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError);
2397
2421
 
2398
2422
  // Cancel pending deferreds for no-longer-matched routes or routes we're
2399
2423
  // about to reload. Note that if this is an action reload we would have
@@ -2425,7 +2449,9 @@ function createRouter(init) {
2425
2449
  // state. If not, we need to switch to our loading state and load data,
2426
2450
  // preserving any new action data or existing action data (in the case of
2427
2451
  // a revalidation interrupting an actionReload)
2428
- if (!isUninterruptedRevalidation) {
2452
+ // If we have partialHydration enabled, then don't update the state for the
2453
+ // initial data load since iot's not a "navigation"
2454
+ if (!isUninterruptedRevalidation && (!future.v7_partialHydration || !initialHydration)) {
2429
2455
  revalidatingFetchers.forEach(rf => {
2430
2456
  let fetcher = state.fetchers.get(rf.key);
2431
2457
  let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
@@ -2534,7 +2560,7 @@ function createRouter(init) {
2534
2560
  if (fetchControllers.has(key)) abortFetcher(key);
2535
2561
  let flushSync = (opts && opts.unstable_flushSync) === true;
2536
2562
  let routesToUse = inFlightDataRoutes || dataRoutes;
2537
- let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, routeId, opts == null ? void 0 : opts.relative);
2563
+ let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, future.v7_relativeSplatPath, routeId, opts == null ? void 0 : opts.relative);
2538
2564
  let matches = matchRoutes(routesToUse, normalizedPath, basename);
2539
2565
  if (!matches) {
2540
2566
  setFetcherError(key, routeId, getInternalRouterError(404, {
@@ -2599,7 +2625,7 @@ function createRouter(init) {
2599
2625
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2600
2626
  fetchControllers.set(key, abortController);
2601
2627
  let originatingLoadId = incrementingLoadId;
2602
- let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
2628
+ let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2603
2629
  if (fetchRequest.signal.aborted) {
2604
2630
  // We can delete this so long as we weren't aborted by our own fetcher
2605
2631
  // re-submit which would have put _new_ controller is in fetchControllers
@@ -2652,7 +2678,7 @@ function createRouter(init) {
2652
2678
  fetchReloadIds.set(key, loadId);
2653
2679
  let loadFetcher = getLoadingFetcher(submission, actionResult.data);
2654
2680
  state.fetchers.set(key, loadFetcher);
2655
- let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, {
2681
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, false, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, {
2656
2682
  [match.route.id]: actionResult.data
2657
2683
  }, undefined // No need to send through errors since we short circuit above
2658
2684
  );
@@ -2752,7 +2778,7 @@ function createRouter(init) {
2752
2778
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2753
2779
  fetchControllers.set(key, abortController);
2754
2780
  let originatingLoadId = incrementingLoadId;
2755
- let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename);
2781
+ let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2756
2782
 
2757
2783
  // Deferred isn't supported for fetcher loads, await everything and treat it
2758
2784
  // as a normal load. resolveDeferredData will return undefined if this
@@ -2900,9 +2926,9 @@ function createRouter(init) {
2900
2926
  // Call all navigation loaders and revalidating fetcher loaders in parallel,
2901
2927
  // then slice off the results into separate arrays so we can handle them
2902
2928
  // accordingly
2903
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename)), ...fetchersToLoad.map(f => {
2929
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath)), ...fetchersToLoad.map(f => {
2904
2930
  if (f.matches && f.match && f.controller) {
2905
- return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename);
2931
+ return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2906
2932
  } else {
2907
2933
  let error = {
2908
2934
  type: ResultType.error,
@@ -3179,6 +3205,9 @@ function createRouter(init) {
3179
3205
  get basename() {
3180
3206
  return basename;
3181
3207
  },
3208
+ get future() {
3209
+ return future;
3210
+ },
3182
3211
  get state() {
3183
3212
  return state;
3184
3213
  },
@@ -3218,6 +3247,11 @@ function createRouter(init) {
3218
3247
  ////////////////////////////////////////////////////////////////////////////////
3219
3248
 
3220
3249
  const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
3250
+
3251
+ /**
3252
+ * Future flags to toggle new feature behavior
3253
+ */
3254
+
3221
3255
  function createStaticHandler(routes, opts) {
3222
3256
  invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
3223
3257
  let manifest = {};
@@ -3234,6 +3268,10 @@ function createStaticHandler(routes, opts) {
3234
3268
  } else {
3235
3269
  mapRouteProperties = defaultMapRouteProperties;
3236
3270
  }
3271
+ // Config driven behavior flags
3272
+ let future = _extends({
3273
+ v7_relativeSplatPath: false
3274
+ }, opts ? opts.future : null);
3237
3275
  let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
3238
3276
 
3239
3277
  /**
@@ -3449,7 +3487,7 @@ function createStaticHandler(routes, opts) {
3449
3487
  error
3450
3488
  };
3451
3489
  } else {
3452
- result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, {
3490
+ result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3453
3491
  isStaticRequest: true,
3454
3492
  isRouteRequest,
3455
3493
  requestContext
@@ -3568,7 +3606,7 @@ function createStaticHandler(routes, opts) {
3568
3606
  activeDeferreds: null
3569
3607
  };
3570
3608
  }
3571
- let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, {
3609
+ let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3572
3610
  isStaticRequest: true,
3573
3611
  isRouteRequest,
3574
3612
  requestContext
@@ -3623,7 +3661,7 @@ function getStaticContextFromError(routes, context, error) {
3623
3661
  function isSubmissionNavigation(opts) {
3624
3662
  return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
3625
3663
  }
3626
- function normalizeTo(location, matches, basename, prependBasename, to, fromRouteId, relative) {
3664
+ function normalizeTo(location, matches, basename, prependBasename, to, v7_relativeSplatPath, fromRouteId, relative) {
3627
3665
  let contextualMatches;
3628
3666
  let activeRouteMatch;
3629
3667
  if (fromRouteId) {
@@ -3643,7 +3681,7 @@ function normalizeTo(location, matches, basename, prependBasename, to, fromRoute
3643
3681
  }
3644
3682
 
3645
3683
  // Resolve the relative path
3646
- let path = resolveTo(to ? to : ".", getPathContributingMatches(contextualMatches).map(m => m.pathnameBase), stripBasename(location.pathname, basename) || location.pathname, relative === "path");
3684
+ let path = resolveTo(to ? to : ".", getResolveToMatches(contextualMatches, v7_relativeSplatPath), stripBasename(location.pathname, basename) || location.pathname, relative === "path");
3647
3685
 
3648
3686
  // When `to` is not specified we inherit search/hash from the current
3649
3687
  // location, unlike when to="." and we just inherit the path.
@@ -3807,7 +3845,7 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3807
3845
  }
3808
3846
  return boundaryMatches;
3809
3847
  }
3810
- function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError) {
3848
+ function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError) {
3811
3849
  let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
3812
3850
  let currentUrl = history.createURL(state.location);
3813
3851
  let nextUrl = history.createURL(location);
@@ -3816,6 +3854,11 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3816
3854
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3817
3855
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3818
3856
  let navigationMatches = boundaryMatches.filter((match, index) => {
3857
+ if (isInitialLoad) {
3858
+ // On initial hydration we don't do any shouldRevalidate stuff - we just
3859
+ // call the unhydrated loaders
3860
+ return isUnhydratedRoute(state, match.route);
3861
+ }
3819
3862
  if (match.route.lazy) {
3820
3863
  // We haven't loaded this route yet so we don't know if it's got a loader!
3821
3864
  return true;
@@ -3855,8 +3898,12 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3855
3898
  // Pick fetcher.loads that need to be revalidated
3856
3899
  let revalidatingFetchers = [];
3857
3900
  fetchLoadMatches.forEach((f, key) => {
3858
- // Don't revalidate if fetcher won't be present in the subsequent render
3859
- if (!matches.some(m => m.route.id === f.routeId) || deletedFetchers.has(key)) {
3901
+ // Don't revalidate:
3902
+ // - on initial load (shouldn't be any fetchers then anyway)
3903
+ // - if fetcher won't be present in the subsequent render
3904
+ // - no longer matches the URL (v7_fetcherPersist=false)
3905
+ // - was unmounted but persisted due to v7_fetcherPersist=true
3906
+ if (isInitialLoad || !matches.some(m => m.route.id === f.routeId) || deletedFetchers.has(key)) {
3860
3907
  return;
3861
3908
  }
3862
3909
  let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
@@ -3920,6 +3967,20 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3920
3967
  });
3921
3968
  return [navigationMatches, revalidatingFetchers];
3922
3969
  }
3970
+
3971
+ // Is this route unhydrated (when v7_partialHydration=true) such that we need
3972
+ // to call it's loader on the initial router creation
3973
+ function isUnhydratedRoute(state, route) {
3974
+ if (!route.loader) {
3975
+ return false;
3976
+ }
3977
+ if (route.loader.hydrate) {
3978
+ return true;
3979
+ }
3980
+ return state.loaderData[route.id] === undefined && (!state.errors ||
3981
+ // Loader ran but errored - don't re-run
3982
+ state.errors[route.id] === undefined);
3983
+ }
3923
3984
  function isNewLoader(currentLoaderData, currentMatch, match) {
3924
3985
  let isNew =
3925
3986
  // [a] -> [a, b]
@@ -4006,7 +4067,7 @@ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
4006
4067
  lazy: undefined
4007
4068
  }));
4008
4069
  }
4009
- async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, opts) {
4070
+ async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, v7_relativeSplatPath, opts) {
4010
4071
  if (opts === void 0) {
4011
4072
  opts = {};
4012
4073
  }
@@ -4096,7 +4157,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4096
4157
 
4097
4158
  // Support relative routing in internal redirects
4098
4159
  if (!ABSOLUTE_URL_REGEX.test(location)) {
4099
- location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location);
4160
+ location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location, v7_relativeSplatPath);
4100
4161
  } else if (!opts.isStaticRequest) {
4101
4162
  // Strip off the protocol+origin for same-origin + same-basename absolute
4102
4163
  // redirects. If this is a static request, we can let it go back to the
@@ -4137,13 +4198,20 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
4137
4198
  throw queryRouteResponse;
4138
4199
  }
4139
4200
  let data;
4140
- let contentType = result.headers.get("Content-Type");
4141
- // Check between word boundaries instead of startsWith() due to the last
4142
- // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
4143
- if (contentType && /\bapplication\/json\b/.test(contentType)) {
4144
- data = await result.json();
4145
- } else {
4146
- data = await result.text();
4201
+ try {
4202
+ let contentType = result.headers.get("Content-Type");
4203
+ // Check between word boundaries instead of startsWith() due to the last
4204
+ // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
4205
+ if (contentType && /\bapplication\/json\b/.test(contentType)) {
4206
+ data = await result.json();
4207
+ } else {
4208
+ data = await result.text();
4209
+ }
4210
+ } catch (e) {
4211
+ return {
4212
+ type: ResultType.error,
4213
+ error: e
4214
+ };
4147
4215
  }
4148
4216
  if (resultType === ResultType.error) {
4149
4217
  return {
@@ -4742,7 +4810,7 @@ exports.UNSAFE_DeferredData = DeferredData;
4742
4810
  exports.UNSAFE_ErrorResponseImpl = ErrorResponseImpl;
4743
4811
  exports.UNSAFE_convertRouteMatchToUiMatch = convertRouteMatchToUiMatch;
4744
4812
  exports.UNSAFE_convertRoutesToDataRoutes = convertRoutesToDataRoutes;
4745
- exports.UNSAFE_getPathContributingMatches = getPathContributingMatches;
4813
+ exports.UNSAFE_getResolveToMatches = getResolveToMatches;
4746
4814
  exports.UNSAFE_invariant = invariant;
4747
4815
  exports.UNSAFE_warning = warning;
4748
4816
  exports.createBrowserHistory = createBrowserHistory;