Skip to content

Commit 0d2a38c

Browse files
authored
Support partial hydration for Remix clientLoader/clientAction (#11033)
1 parent cb53f41 commit 0d2a38c

22 files changed

+1445
-293
lines changed

.changeset/partial-hydration-data.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@remix-run/router": minor
3+
---
4+
5+
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.
6+
7+
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`.
8+
9+
```jsx
10+
let router = createBrowserRouter(
11+
[
12+
{
13+
id: "root",
14+
path: "/",
15+
loader: rootLoader,
16+
Component: RootComponent,
17+
Fallback: RootFallback,
18+
children: [
19+
{
20+
id: "index",
21+
index: true,
22+
loader: indexLoader,
23+
Component: IndexComponent,
24+
HydrateFallback: IndexFallback,
25+
},
26+
],
27+
},
28+
],
29+
{
30+
future: {
31+
v7_partialHydration: true,
32+
},
33+
hydrationData: {
34+
loaderData: {
35+
root: { message: "Hydrated from Root!" },
36+
},
37+
},
38+
}
39+
);
40+
```
41+
42+
If the above example did not have an `IndexFallback`, then `RouterProvider` would instead render the `RootFallback` while it executed the `indexLoader`.
43+
44+
**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.

docs/guides/api-development-strategy.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,12 @@ const router = createBrowserRouter(routes, {
6363
});
6464
```
6565

66-
| Flag | Description |
67-
| ------------------------ | --------------------------------------------------------------------- |
68-
| `v7_fetcherPersist` | Delay active fetcher cleanup until they return to an `idle` state |
69-
| `v7_normalizeFormMethod` | Normalize `useNavigation().formMethod` to be an uppercase HTTP Method |
70-
| `v7_prependBasename` | Prepend the router basename to navigate/fetch paths |
66+
| Flag | Description |
67+
| ----------------------------------------- | --------------------------------------------------------------------- |
68+
| `v7_fetcherPersist` | Delay active fetcher cleanup until they return to an `idle` state |
69+
| `v7_normalizeFormMethod` | Normalize `useNavigation().formMethod` to be an uppercase HTTP Method |
70+
| [`v7_partialHydration`][partialhydration] | Support partial hydration for Server-rendered apps |
71+
| `v7_prependBasename` | Prepend the router basename to navigate/fetch paths |
7172

7273
### React Router Future Flags
7374

@@ -94,3 +95,4 @@ These flags apply to both Data and non-Data Routers and are passed to the render
9495
[feature-flowchart]: https://remix.run/docs-images/feature-flowchart.png
9596
[picking-a-router]: ../routers/picking-a-router
9697
[starttransition]: https://react.dev/reference/react/startTransition
98+
[partialhydration]: ../routers/create-browser-router#partial-hydration-data

docs/guides/ssr.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ And with that you've got a server-side-rendered and hydrated application! For a
165165

166166
As mentioned above, server-side rendering is tricky at scale and for production-grade applications, and we strongly recommend checking out [Remix][remix] if that's your goal. But if you are going the manual route, here's a few additional concepts you may need to consider:
167167

168+
#### Hydration
169+
170+
A core concept of Server Side Rendering is [hydration][hydration] which involves "attaching" a client-side React application to server-rendered HTML. To do this correctly, we need to create our client-side React Router application in the same state that it was in during the server render. When your server render loaded data via `loader` functions, we need to send this data up so that we can create our client router with the same loader data for the initial render/hydration.
171+
172+
The basic usages of `<StaticRouterProvider>` and `createBrowserRouter` shown in this guide handle this for you internally, but if you need to take control over the hydration process you can disable the automatic hydration process via [`<StaticRouterProvider hydrate={false} />`][hydrate-false].
173+
174+
In some advanced use cases, you may want to partially hydrate a client-side React Router application. You can do this via the [`future.v7_partialHydration`][partialhydration] flag passed to `createBrowserRouter`.
175+
168176
#### Redirects
169177

170178
If any loaders redirect, `handler.query` will return the `Response` directly so you should check that and send a redirect response instead of attempting to render an HTML document:
@@ -309,3 +317,6 @@ Again, we recommend you give [Remix](https://remix.run) a look. It's the best wa
309317
[createstaticrouter]: ../routers/create-static-router
310318
[staticrouterprovider]: ../routers/static-router-provider
311319
[lazy]: ../route/lazy
320+
[hydration]: https://react.dev/reference/react-dom/client/hydrateRoot
321+
[hydrate-false]: ../routers/static-router-provider#hydrate
322+
[partialhydration]: ../routers/create-browser-router#partial-hydration-data
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
title: hydrateFallbackElement
3+
new: true
4+
---
5+
6+
# `hydrateFallbackElement`
7+
8+
If you are using [Server-Side Rendering][ssr] and you are leveraging [partial hydration][partialhydration], then you can specify an Element/Component to render for non-hydrated routes during the initial hydration of the application.
9+
10+
<docs-info>If you do not wish to specify a React element (i.e., `hydrateFallbackElement={<MyFallback />}`) you may specify an `HydrateFallback` component instead (i.e., `HydrateFallback={MyFallback}`) and React Router will call `createElement` for you internally.</docs-info>
11+
12+
<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>
13+
14+
```tsx
15+
let router = createBrowserRouter(
16+
[
17+
{
18+
id: "root",
19+
path: "/",
20+
loader: rootLoader,
21+
Component: Root,
22+
children: [
23+
{
24+
id: "invoice",
25+
path: "invoices/:id",
26+
loader: loadInvoice,
27+
Component: Invoice,
28+
HydrateFallback: InvoiceSkeleton,
29+
},
30+
],
31+
},
32+
],
33+
{
34+
future: {
35+
v7_partialHydration: true,
36+
},
37+
hydrationData: {
38+
root: {
39+
/*...*/
40+
},
41+
// No hydration data provided for the `invoice` route
42+
},
43+
}
44+
);
45+
```
46+
47+
<docs-warning>There is no default fallback and it will just render `null` at that route level, so it is recommended that you always provide your own fallback element.</docs-warning>
48+
49+
[pickingarouter]: ../routers/picking-a-router
50+
[ssr]: ../guides/ssr
51+
[partialhydration]: ../routers/create-browser-router#partial-hydration-data

docs/route/loader.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ function loader({ request }) {
8585

8686
Note that the APIs here are not React Router specific, but rather standard web objects: [Request][request], [URL][url], [URLSearchParams][urlsearchparams].
8787

88+
## `loader.hydrate`
89+
90+
If you are [Server-Side Rendering][ssr] and leveraging the `fututre.v7_partialHydration` flag for [Partial Hydration][partialhydration], then you may wish to opt-into running a route `loader` on initial hydration _even though it has hydration data_ (for example, to let a user prime a cache with the hydration data). To force a `loader` to run on hydration in a partial hydration scenario, you can set a `hydrate` property on the `loader` function:
91+
8892
## Returning Responses
8993

9094
While you can return anything you want from a loader and get access to it from [`useLoaderData`][useloaderdata], you can also return a web [Response][response].
@@ -174,3 +178,5 @@ For more details, read the [`errorElement`][errorelement] documentation.
174178
[json]: ../fetch/json
175179
[errorelement]: ./error-element
176180
[pickingarouter]: ../routers/picking-a-router
181+
[ssr]: ../guides/ssr.md
182+
[partialhydration]: ../routers/create-browser-router#partial-hydration-data

docs/route/route.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ interface RouteObject {
7676
loader?: LoaderFunction;
7777
action?: ActionFunction;
7878
element?: React.ReactNode | null;
79-
Component?: React.ComponentType | null;
79+
hydrateFallbackElement?: React.ReactNode | null;
8080
errorElement?: React.ReactNode | null;
81+
Component?: React.ComponentType | null;
82+
HydrateFallback?: React.ComponentType | null;
8183
ErrorBoundary?: React.ComponentType | null;
8284
handle?: RouteObject["handle"];
8385
shouldRevalidate?: ShouldRevalidateFunction;
@@ -354,6 +356,16 @@ Otherwise use `ErrorBoundary` and React Router will create the React Element for
354356

355357
Please see the [errorElement][errorelement] documentation for more details.
356358

359+
## `hydrateFallbackElement`/`HydrateFallback`
360+
361+
If you are using [Server-Side Rendering][ssr] and you are leveraging [partial hydration][partialhydration], then you can specify an Element/Component to render for non-hydrated routes during the initial hydration of the application.
362+
363+
<docs-warning>If you are not using a data router like [`createBrowserRouter`][createbrowserrouter], this will do nothing</docs-warning>
364+
365+
<docs-warning>This is only intended for more advanced uses cases such as Remix's [`clientLoader`][clientloader] functionality. Most SSR apps will not need to leverage these route properties.</docs-warning>
366+
367+
Please see the [hydrateFallbackElement][hydratefallbackelement] documentation for more details.
368+
357369
## `handle`
358370

359371
Any application-specific data. Please see the [useMatches][usematches] documentation for details and examples.
@@ -404,10 +416,14 @@ Please see the [lazy][lazy] documentation for more details.
404416
[loader]: ./loader
405417
[action]: ./action
406418
[errorelement]: ./error-element
419+
[hydratefallbackelement]: ./hydrate-fallback-element
407420
[form]: ../components/form
408421
[fetcher]: ../hooks/use-fetcher
409422
[usesubmit]: ../hooks/use-submit
410423
[createroutesfromelements]: ../utils/create-routes-from-elements
411424
[createbrowserrouter]: ../routers/create-browser-router
412425
[usematches]: ../hooks/use-matches
413426
[lazy]: ./lazy
427+
[ssr]: ../guides/ssr
428+
[partialhydration]: ../routers/create-browser-router#partial-hydration-data
429+
[clientloader]: https://remix.run/route/client-loader

docs/routers/create-browser-router.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,66 @@ The following future flags are currently available:
120120
| ------------------------ | --------------------------------------------------------------------- |
121121
| `v7_fetcherPersist` | Delay active fetcher cleanup until they return to an `idle` state |
122122
| `v7_normalizeFormMethod` | Normalize `useNavigation().formMethod` to be an uppercase HTTP Method |
123+
| `v7_partialHydration` | Support partial hydration for Server-rendered apps |
123124
| `v7_prependBasename` | Prepend the router basename to navigate/fetch paths |
124125

126+
## `hydrationData`
127+
128+
When [Server-Rendering][ssr] and [opting-out of automatic hydration][hydrate-false], the `hydrationData` option allows you to pass in hydration data from your server-render. This will almost always be a subset of data from the `StaticHandlerContext` value you get back from [handler.query][query]:
129+
130+
```js
131+
const router = createBrowserRouter(routes, {
132+
hydrationData: {
133+
loaderData: {
134+
// [routeId]: serverLoaderData
135+
},
136+
// may also include `errors` and/or `actionData`
137+
},
138+
});
139+
```
140+
141+
### Partial Hydration Data
142+
143+
You will almost always include a complete set of `loaderData` to hydrate a server-rendered app. But in advanced use-cases (such as Remix's [`clientLoader`][clientloader]), you may want to include `loaderData` for only _some_ routes that were rendered on the server. If you want to enable partial `loaderData` and opt-into granular [`route.HydrateFallback`][hydratefallback] usage, you will need to enable the `future.v7_partialHydration` flag. Prior to this flag, any provided `loaderData` was assumed to be complete and would not result in the execution of route loaders on initial hydration.
144+
145+
When this flag is specified, loaders will run on initial hydration in 2 scenarios:
146+
147+
- No hydration data is provided
148+
- In these cases the `HydrateFallback` component will render on initial hydration
149+
- The `loader.hydrate` property is set to `true`
150+
- This allows you to run the `loader` even if you did not render a fallback on initial hydration (i.e., to prime a cache with hydration data)
151+
152+
```js
153+
const router = createBrowserRouter(
154+
[
155+
{
156+
id: "root",
157+
loader: rootLoader,
158+
Component: Root,
159+
children: [
160+
{
161+
id: "index",
162+
loader: indexLoader,
163+
HydrateFallback: IndexSkeleton,
164+
Component: Index,
165+
},
166+
],
167+
},
168+
],
169+
{
170+
future: {
171+
v7_partialHydration: true,
172+
},
173+
hydrationData: {
174+
loaderData: {
175+
root: "ROOT DATA",
176+
// No index data provided
177+
},
178+
},
179+
}
180+
);
181+
```
182+
125183
## `window`
126184

127185
Useful for environments like browser devtool plugins or testing to use a different window than the global `window`.
@@ -134,3 +192,8 @@ Useful for environments like browser devtool plugins or testing to use a differe
134192
[api-development-strategy]: ../guides/api-development-strategy
135193
[remixing-react-router]: https://remix.run/blog/remixing-react-router
136194
[when-to-fetch]: https://www.youtube.com/watch?v=95B8mnhzoCM
195+
[ssr]: ../guides/ssr
196+
[hydrate-false]: ../routers/static-router-provider#hydrate
197+
[query]: ./create-static-handler#handlerqueryrequest-opts
198+
[clientloader]: https://remix.run/route/client-loader
199+
[hydratefallback]: ../route/hydrate-fallback-element

docs/routers/create-static-router.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,34 @@ export async function renderHtml(req) {
5555
```ts
5656
declare function createStaticRouter(
5757
routes: RouteObject[],
58-
context: StaticHandlerContext
58+
context: StaticHandlerContext,
59+
opts: {
60+
future?: {
61+
v7_partialHydration?: boolean;
62+
};
63+
}
5964
): Router;
6065
```
6166

67+
## `opts.future`
68+
69+
An optional set of [Future Flags][api-development-strategy] to enable for this Static Router. We recommend opting into newly released future flags sooner rather than later to ease your eventual migration to v7.
70+
71+
```js
72+
const router = createBrowserRouter(routes, {
73+
future: {
74+
// Opt-into partial hydration
75+
v7_partialHydration: true,
76+
},
77+
});
78+
```
79+
80+
The following future flags are currently available:
81+
82+
| Flag | Description |
83+
| ----------------------------------------- | -------------------------------------------------- |
84+
| [`v7_partialHydration`][partialhydration] | Support partial hydration for Server-rendered apps |
85+
6286
**See also:**
6387

6488
- [`createStaticHandler`][createstatichandler]
@@ -69,3 +93,5 @@ declare function createStaticRouter(
6993
[ssr]: ../guides/ssr
7094
[createstatichandler]: ../routers/create-static-handler
7195
[staticrouterprovider]: ../routers/static-router-provider
96+
[partialhydration]: ../routers/create-browser-router#partial-hydration-data
97+
[api-development-strategy]: ../guides/api-development-strategy

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@
110110
},
111111
"filesize": {
112112
"packages/router/dist/router.umd.min.js": {
113-
"none": "49.4 kB"
113+
"none": "49.8 kB"
114114
},
115115
"packages/react-router/dist/react-router.production.min.js": {
116-
"none": "13.9 kB"
116+
"none": "14.3 kB"
117117
},
118118
"packages/react-router/dist/umd/react-router.production.min.js": {
119-
"none": "16.3 kB"
119+
"none": "16.8 kB"
120120
},
121121
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
122122
"none": "16.7 kB"

0 commit comments

Comments
 (0)