Skip to content

Commit

Permalink
Support partial hydration for Remix clientLoader/clientAction (#11033)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Dec 4, 2023
1 parent cb53f41 commit 0d2a38c
Show file tree
Hide file tree
Showing 22 changed files with 1,445 additions and 293 deletions.
44 changes: 44 additions & 0 deletions .changeset/partial-hydration-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
"@remix-run/router": minor
---

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.

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`.

```jsx
let router = createBrowserRouter(
[
{
id: "root",
path: "/",
loader: rootLoader,
Component: RootComponent,
Fallback: RootFallback,
children: [
{
id: "index",
index: true,
loader: indexLoader,
Component: IndexComponent,
HydrateFallback: IndexFallback,
},
],
},
],
{
future: {
v7_partialHydration: true,
},
hydrationData: {
loaderData: {
root: { message: "Hydrated from Root!" },
},
},
}
);
```

If the above example did not have an `IndexFallback`, then `RouterProvider` would instead render the `RootFallback` while it executed the `indexLoader`.

**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.
12 changes: 7 additions & 5 deletions docs/guides/api-development-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ const router = createBrowserRouter(routes, {
});
```

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

### React Router Future Flags

Expand All @@ -94,3 +95,4 @@ These flags apply to both Data and non-Data Routers and are passed to the render
[feature-flowchart]: https://remix.run/docs-images/feature-flowchart.png
[picking-a-router]: ../routers/picking-a-router
[starttransition]: https://react.dev/reference/react/startTransition
[partialhydration]: ../routers/create-browser-router#partial-hydration-data
11 changes: 11 additions & 0 deletions docs/guides/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ And with that you've got a server-side-rendered and hydrated application! For a

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:

#### Hydration

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.

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].

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`.

#### Redirects

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:
Expand Down Expand Up @@ -309,3 +317,6 @@ Again, we recommend you give [Remix](https://remix.run) a look. It's the best wa
[createstaticrouter]: ../routers/create-static-router
[staticrouterprovider]: ../routers/static-router-provider
[lazy]: ../route/lazy
[hydration]: https://react.dev/reference/react-dom/client/hydrateRoot
[hydrate-false]: ../routers/static-router-provider#hydrate
[partialhydration]: ../routers/create-browser-router#partial-hydration-data
51 changes: 51 additions & 0 deletions docs/route/hydrate-fallback-element.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
title: hydrateFallbackElement
new: true
---

# `hydrateFallbackElement`

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.

<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>

<docs-warning>This feature only works if using a data router, see [Picking a Router][pickingarouter]</docs-warning>

```tsx
let router = createBrowserRouter(
[
{
id: "root",
path: "/",
loader: rootLoader,
Component: Root,
children: [
{
id: "invoice",
path: "invoices/:id",
loader: loadInvoice,
Component: Invoice,
HydrateFallback: InvoiceSkeleton,
},
],
},
],
{
future: {
v7_partialHydration: true,
},
hydrationData: {
root: {
/*...*/
},
// No hydration data provided for the `invoice` route
},
}
);
```

<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>

[pickingarouter]: ../routers/picking-a-router
[ssr]: ../guides/ssr
[partialhydration]: ../routers/create-browser-router#partial-hydration-data
6 changes: 6 additions & 0 deletions docs/route/loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ function loader({ request }) {

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

## `loader.hydrate`

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:

## Returning Responses

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].
Expand Down Expand Up @@ -174,3 +178,5 @@ For more details, read the [`errorElement`][errorelement] documentation.
[json]: ../fetch/json
[errorelement]: ./error-element
[pickingarouter]: ../routers/picking-a-router
[ssr]: ../guides/ssr.md
[partialhydration]: ../routers/create-browser-router#partial-hydration-data
18 changes: 17 additions & 1 deletion docs/route/route.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ interface RouteObject {
loader?: LoaderFunction;
action?: ActionFunction;
element?: React.ReactNode | null;
Component?: React.ComponentType | null;
hydrateFallbackElement?: React.ReactNode | null;
errorElement?: React.ReactNode | null;
Component?: React.ComponentType | null;
HydrateFallback?: React.ComponentType | null;
ErrorBoundary?: React.ComponentType | null;
handle?: RouteObject["handle"];
shouldRevalidate?: ShouldRevalidateFunction;
Expand Down Expand Up @@ -354,6 +356,16 @@ Otherwise use `ErrorBoundary` and React Router will create the React Element for

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

## `hydrateFallbackElement`/`HydrateFallback`

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.

<docs-warning>If you are not using a data router like [`createBrowserRouter`][createbrowserrouter], this will do nothing</docs-warning>

<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>

Please see the [hydrateFallbackElement][hydratefallbackelement] documentation for more details.

## `handle`

Any application-specific data. Please see the [useMatches][usematches] documentation for details and examples.
Expand Down Expand Up @@ -404,10 +416,14 @@ Please see the [lazy][lazy] documentation for more details.
[loader]: ./loader
[action]: ./action
[errorelement]: ./error-element
[hydratefallbackelement]: ./hydrate-fallback-element
[form]: ../components/form
[fetcher]: ../hooks/use-fetcher
[usesubmit]: ../hooks/use-submit
[createroutesfromelements]: ../utils/create-routes-from-elements
[createbrowserrouter]: ../routers/create-browser-router
[usematches]: ../hooks/use-matches
[lazy]: ./lazy
[ssr]: ../guides/ssr
[partialhydration]: ../routers/create-browser-router#partial-hydration-data
[clientloader]: https://remix.run/route/client-loader
63 changes: 63 additions & 0 deletions docs/routers/create-browser-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,66 @@ The following future flags are currently available:
| ------------------------ | --------------------------------------------------------------------- |
| `v7_fetcherPersist` | Delay active fetcher cleanup until they return to an `idle` state |
| `v7_normalizeFormMethod` | Normalize `useNavigation().formMethod` to be an uppercase HTTP Method |
| `v7_partialHydration` | Support partial hydration for Server-rendered apps |
| `v7_prependBasename` | Prepend the router basename to navigate/fetch paths |

## `hydrationData`

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]:

```js
const router = createBrowserRouter(routes, {
hydrationData: {
loaderData: {
// [routeId]: serverLoaderData
},
// may also include `errors` and/or `actionData`
},
});
```

### Partial Hydration Data

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.

When this flag is specified, loaders will run on initial hydration in 2 scenarios:

- No hydration data is provided
- In these cases the `HydrateFallback` component will render on initial hydration
- The `loader.hydrate` property is set to `true`
- 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)

```js
const router = createBrowserRouter(
[
{
id: "root",
loader: rootLoader,
Component: Root,
children: [
{
id: "index",
loader: indexLoader,
HydrateFallback: IndexSkeleton,
Component: Index,
},
],
},
],
{
future: {
v7_partialHydration: true,
},
hydrationData: {
loaderData: {
root: "ROOT DATA",
// No index data provided
},
},
}
);
```

## `window`

Useful for environments like browser devtool plugins or testing to use a different window than the global `window`.
Expand All @@ -134,3 +192,8 @@ Useful for environments like browser devtool plugins or testing to use a differe
[api-development-strategy]: ../guides/api-development-strategy
[remixing-react-router]: https://remix.run/blog/remixing-react-router
[when-to-fetch]: https://www.youtube.com/watch?v=95B8mnhzoCM
[ssr]: ../guides/ssr
[hydrate-false]: ../routers/static-router-provider#hydrate
[query]: ./create-static-handler#handlerqueryrequest-opts
[clientloader]: https://remix.run/route/client-loader
[hydratefallback]: ../route/hydrate-fallback-element
28 changes: 27 additions & 1 deletion docs/routers/create-static-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,34 @@ export async function renderHtml(req) {
```ts
declare function createStaticRouter(
routes: RouteObject[],
context: StaticHandlerContext
context: StaticHandlerContext,
opts: {
future?: {
v7_partialHydration?: boolean;
};
}
): Router;
```

## `opts.future`

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.

```js
const router = createBrowserRouter(routes, {
future: {
// Opt-into partial hydration
v7_partialHydration: true,
},
});
```

The following future flags are currently available:

| Flag | Description |
| ----------------------------------------- | -------------------------------------------------- |
| [`v7_partialHydration`][partialhydration] | Support partial hydration for Server-rendered apps |

**See also:**

- [`createStaticHandler`][createstatichandler]
Expand All @@ -69,3 +93,5 @@ declare function createStaticRouter(
[ssr]: ../guides/ssr
[createstatichandler]: ../routers/create-static-handler
[staticrouterprovider]: ../routers/static-router-provider
[partialhydration]: ../routers/create-browser-router#partial-hydration-data
[api-development-strategy]: ../guides/api-development-strategy
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,13 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "49.4 kB"
"none": "49.8 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "13.9 kB"
"none": "14.3 kB"
},
"packages/react-router/dist/umd/react-router.production.min.js": {
"none": "16.3 kB"
"none": "16.8 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "16.7 kB"
Expand Down

0 comments on commit 0d2a38c

Please sign in to comment.