@remix-run/router 0.0.0-experimental-9463fb5e → 0.0.0-experimental-8bb3ffdf

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
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 implement 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/pull/11078) 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 path 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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v0.0.0-experimental-9463fb5e
2
+ * @remix-run/router v0.0.0-experimental-8bb3ffdf
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1715,18 +1715,28 @@ function createRouter(init) {
1715
1715
  [route.id]: error
1716
1716
  };
1717
1717
  }
1718
-
1719
- // "Initialized" here really means "Can `RouterProvider` render my route tree?"
1720
- // Prior to `route.HydrateFallback`, we only had a root `fallbackElement` so we used
1721
- // `state.initialized` to render that instead of `<DataRoutes>`. Now that we
1722
- // support route level fallbacks we can always render and we'll just render
1723
- // as deep as we have data for and detect the nearest ancestor HydrateFallback
1724
- let initialized = future.v7_partialHydration ||
1725
- // All initialMatches need to be loaded before we're ready. If we have lazy
1726
- // functions around still then we'll need to run them in initialize()
1727
- !initialMatches.some(m => m.route.lazy) && (
1728
- // And we have to either have no loaders or have been provided hydrationData
1729
- !initialMatches.some(m => m.route.loader) || init.hydrationData != null);
1718
+ let initialized;
1719
+ let hasLazyRoutes = initialMatches.some(m => m.route.lazy);
1720
+ let hasLoaders = initialMatches.some(m => m.route.loader);
1721
+ if (hasLazyRoutes) {
1722
+ // All initialMatches need to be loaded before we're ready. If we have lazy
1723
+ // functions around still then we'll need to run them in initialize()
1724
+ initialized = false;
1725
+ } else if (!hasLoaders) {
1726
+ // If we've got no loaders to run, then we're good to go
1727
+ initialized = true;
1728
+ } else if (future.v7_partialHydration) {
1729
+ // If partial hydration is enabled, we're initialized so long as we were
1730
+ // provided with hydrationData for every route with a loader, and no loaders
1731
+ // were marked for explicit hydration
1732
+ let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
1733
+ let errors = init.hydrationData ? init.hydrationData.errors : null;
1734
+ initialized = initialMatches.every(m => m.route.loader && m.route.loader.hydrate !== true && (loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined));
1735
+ } else {
1736
+ // Without partial hydration - we're initialized if we were provided any
1737
+ // hydrationData - which is expected to be complete
1738
+ initialized = init.hydrationData != null;
1739
+ }
1730
1740
  let router;
1731
1741
  let state = {
1732
1742
  historyAction: init.history.action,
@@ -1893,7 +1903,7 @@ function createRouter(init) {
1893
1903
  // in the normal navigation flow. For SSR it's expected that lazy modules are
1894
1904
  // resolved prior to router creation since we can't go into a fallbackElement
1895
1905
  // UI for SSR'd apps
1896
- if (!state.initialized || future.v7_partialHydration && state.matches.some(m => isUnhydratedRoute(state, m.route))) {
1906
+ if (!state.initialized) {
1897
1907
  startNavigation(Action.Pop, state.location, {
1898
1908
  initialHydration: true
1899
1909
  });
@@ -3265,8 +3275,7 @@ function createStaticHandler(routes, opts) {
3265
3275
  }
3266
3276
  // Config driven behavior flags
3267
3277
  let future = _extends({
3268
- v7_relativeSplatPath: false,
3269
- v7_throwAbortReason: false
3278
+ v7_relativeSplatPath: false
3270
3279
  }, opts ? opts.future : null);
3271
3280
  let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
3272
3281
 
@@ -3489,7 +3498,8 @@ function createStaticHandler(routes, opts) {
3489
3498
  requestContext
3490
3499
  });
3491
3500
  if (request.signal.aborted) {
3492
- throwStaticHandlerAbortedError(request, isRouteRequest, future);
3501
+ let method = isRouteRequest ? "queryRoute" : "query";
3502
+ throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3493
3503
  }
3494
3504
  }
3495
3505
  if (isRedirectResult(result)) {
@@ -3607,7 +3617,8 @@ function createStaticHandler(routes, opts) {
3607
3617
  requestContext
3608
3618
  }))]);
3609
3619
  if (request.signal.aborted) {
3610
- throwStaticHandlerAbortedError(request, isRouteRequest, future);
3620
+ let method = isRouteRequest ? "queryRoute" : "query";
3621
+ throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3611
3622
  }
3612
3623
 
3613
3624
  // Process and commit output from loaders
@@ -3652,14 +3663,6 @@ function getStaticContextFromError(routes, context, error) {
3652
3663
  });
3653
3664
  return newContext;
3654
3665
  }
3655
- function throwStaticHandlerAbortedError(request, isRouteRequest, future) {
3656
- let method = isRouteRequest ? "queryRoute" : "query";
3657
- if (future.v7_throwAbortReason && request.signal.reason !== undefined) {
3658
- throw request.signal.reason;
3659
- } else {
3660
- throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3661
- }
3662
- }
3663
3666
  function isSubmissionNavigation(opts) {
3664
3667
  return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
3665
3668
  }
@@ -3856,18 +3859,24 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3856
3859
  let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3857
3860
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3858
3861
  let navigationMatches = boundaryMatches.filter((match, index) => {
3859
- if (isInitialLoad) {
3860
- // On initial hydration we don't do any shouldRevalidate stuff - we just
3861
- // call the unhydrated loaders
3862
- return isUnhydratedRoute(state, match.route);
3863
- }
3864
- if (match.route.lazy) {
3862
+ let {
3863
+ route
3864
+ } = match;
3865
+ if (route.lazy) {
3865
3866
  // We haven't loaded this route yet so we don't know if it's got a loader!
3866
3867
  return true;
3867
3868
  }
3868
- if (match.route.loader == null) {
3869
+ if (route.loader == null) {
3869
3870
  return false;
3870
3871
  }
3872
+ if (isInitialLoad) {
3873
+ if (route.loader.hydrate) {
3874
+ return true;
3875
+ }
3876
+ return state.loaderData[route.id] === undefined && (
3877
+ // Don't re-run if the loader ran and threw an error
3878
+ !state.errors || state.errors[route.id] === undefined);
3879
+ }
3871
3880
 
3872
3881
  // Always call the loader on new route instances and pending defer cancellations
3873
3882
  if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {
@@ -3969,20 +3978,6 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
3969
3978
  });
3970
3979
  return [navigationMatches, revalidatingFetchers];
3971
3980
  }
3972
-
3973
- // Is this route unhydrated (when v7_partialHydration=true) such that we need
3974
- // to call it's loader on the initial router creation
3975
- function isUnhydratedRoute(state, route) {
3976
- if (!route.loader) {
3977
- return false;
3978
- }
3979
- if (route.loader.hydrate) {
3980
- return true;
3981
- }
3982
- return state.loaderData[route.id] === undefined && (!state.errors ||
3983
- // Loader ran but errored - don't re-run
3984
- state.errors[route.id] === undefined);
3985
- }
3986
3981
  function isNewLoader(currentLoaderData, currentMatch, match) {
3987
3982
  let isNew =
3988
3983
  // [a] -> [a, b]