Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support partial hydration for Remix clientLoader/clientAction #11033

Merged
merged 19 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading