Skip to content

Commit

Permalink
Allow loader.hydrate to be set
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Nov 30, 2023
1 parent 567cfa9 commit 35fa15e
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 5 deletions.
3 changes: 3 additions & 0 deletions docs/guides/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ A core concept of Server Side Rendering is [hydration][hydration] which involves

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 @@ -317,3 +319,4 @@ Again, we recommend you give [Remix](https://remix.run) a look. It's the best wa
[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
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
38 changes: 38 additions & 0 deletions docs/routers/create-browser-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,44 @@ const router = createBrowserRouter(routes, {

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 Down
83 changes: 81 additions & 2 deletions packages/router/__tests__/route-fallback-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Router } from "../index";
import type { LoaderFunction, Router } from "../index";
import { IDLE_NAVIGATION, createMemoryHistory, createRouter } from "../index";

import {
Expand Down Expand Up @@ -251,7 +251,7 @@ describe("future.v7_partialHydration", () => {
});

describe("when set to true", () => {
it("starts with initialized=true when loaders exist with partial hydrationData", async () => {
it("starts with initialized=true, runs unhydrated loaders with partial hydrationData", async () => {
let spy = jest.fn();
let shouldRevalidateSpy = jest.fn((args) => args.defaultShouldRevalidate);
let dfd = createDeferred();
Expand Down Expand Up @@ -322,6 +322,85 @@ describe("future.v7_partialHydration", () => {
});
});

it("starts with initialized=true, runs hydrated loaders when loader.hydrate=true", async () => {
let spy = jest.fn();
let shouldRevalidateSpy = jest.fn((args) => args.defaultShouldRevalidate);
let dfd = createDeferred();
let indexLoader: LoaderFunction = () => dfd.promise;
indexLoader.hydrate = true;
router = createRouter({
routes: [
{
id: "root",
path: "/",
loader: spy,
shouldRevalidate: shouldRevalidateSpy,
children: [
{
id: "index",
index: true,
loader: indexLoader,
},
],
},
],
history: createMemoryHistory(),
hydrationData: {
loaderData: {
root: "LOADER DATA",
index: "INDEX INITIAL",
},
},
future: {
v7_partialHydration: true,
},
});

let subscriberSpy = jest.fn();
router.subscribe(subscriberSpy);

// Start with initialized:false
expect(router.state).toMatchObject({
historyAction: "POP",
location: { pathname: "/" },
loaderData: {
root: "LOADER DATA",
index: "INDEX INITIAL",
},
initialized: true,
navigation: { state: "idle" },
});

// Initialize/kick off data loads due to partial hydrationData
router.initialize();
await dfd.resolve("INDEX UPDATED");
expect(router.state).toMatchObject({
historyAction: "POP",
location: { pathname: "/" },
loaderData: {
root: "LOADER DATA",
index: "INDEX UPDATED",
},
initialized: true,
navigation: { state: "idle" },
});

// Root was not re-called
expect(shouldRevalidateSpy).not.toHaveBeenCalled();
expect(spy).not.toHaveBeenCalled();

// Ensure we don't go into a navigating state during initial calls of
// the loaders
expect(subscriberSpy).toHaveBeenCalledTimes(1);
expect(subscriberSpy.mock.calls[0][0]).toMatchObject({
loaderData: {
index: "INDEX UPDATED",
root: "LOADER DATA",
},
navigation: IDLE_NAVIGATION,
});
});

it("does not kick off initial data load if errors exist", async () => {
let consoleWarnSpy = jest
.spyOn(console, "warn")
Expand Down
7 changes: 6 additions & 1 deletion packages/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3748,8 +3748,13 @@ function getMatchesToLoad(
// Is this route unhydrated (when v7_partialHydration=true) such that we need
// to call it's loader on the initial router creation
function isUnhydratedRoute(state: RouterState, route: AgnosticDataRouteObject) {
if (!route.loader) {
return false;
}
if (route.loader.hydrate) {
return true;
}
return (
route.loader != null &&
state.loaderData[route.id] === undefined &&
(!state.errors ||
// Loader ran but errored - don't re-run
Expand Down
4 changes: 2 additions & 2 deletions packages/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,11 @@ type DataFunctionValue = Response | NonNullable<unknown> | null;
/**
* Route loader function signature
*/
export interface LoaderFunction<Context = any> {
export type LoaderFunction<Context = any> = {
(args: LoaderFunctionArgs<Context>):
| Promise<DataFunctionValue>
| DataFunctionValue;
}
} & { hydrate?: boolean };

/**
* Route action function signature
Expand Down

0 comments on commit 35fa15e

Please sign in to comment.