Skip to content

Commit c63b509

Browse files
Skip route.lazy hydration properties after hydration (#13376)
* Skip `route.lazy` hydration properties after hydration * Refactor to use `lazyRoutePropertiesToSkip` array * Refactor to make `initialHydration` an optional arg * Update tests
1 parent a87b796 commit c63b509

File tree

8 files changed

+254
-14
lines changed

8 files changed

+254
-14
lines changed

.changeset/happy-spoons-watch.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
When using the object-based `route.lazy` API, the `HydrateFallback` and `hydrateFallbackElement` properties are now skipped when lazy loading routes after hydration.
6+
7+
If you move the code for these properties into a separate file, you can use this optimization to avoid downloading unused hydration code. For example:
8+
9+
```ts
10+
createBrowserRouter([
11+
{
12+
path: "/show/:showId",
13+
lazy: {
14+
loader: async () => (await import("./show.loader.js")).loader,
15+
Component: async () => (await import("./show.component.js")).Component,
16+
HydrateFallback: async () =>
17+
(await import("./show.hydrate-fallback.js")).HydrateFallback,
18+
},
19+
},
20+
]);
21+
```

packages/react-router/__tests__/router/lazy-test.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { createMemoryHistory } from "../../lib/router/history";
22
import { createRouter, createStaticHandler } from "../../lib/router/router";
3+
import {
4+
createMemoryRouter,
5+
hydrationRouteProperties,
6+
} from "../../lib/components";
37

48
import type {
59
TestNonIndexRouteObject,
@@ -561,6 +565,69 @@ describe("lazily loaded route modules", () => {
561565
expect(t.router.state.matches[0].route.action).toBeUndefined();
562566
});
563567

568+
it("only resolves lazy hydration route properties on hydration", async () => {
569+
let [lazyLoaderForHydration, lazyLoaderDeferredForHydration] =
570+
createAsyncStub();
571+
let [lazyLoaderForNavigation, lazyLoaderDeferredForNavigation] =
572+
createAsyncStub();
573+
let [
574+
lazyHydrateFallbackForHydration,
575+
lazyHydrateFallbackDeferredForHydration,
576+
] = createAsyncStub();
577+
let [
578+
lazyHydrateFallbackElementForHydration,
579+
lazyHydrateFallbackElementDeferredForHydration,
580+
] = createAsyncStub();
581+
let lazyHydrateFallbackForNavigation = jest.fn(async () => null);
582+
let lazyHydrateFallbackElementForNavigation = jest.fn(async () => null);
583+
let router = createMemoryRouter(
584+
[
585+
{
586+
path: "/hydration",
587+
lazy: {
588+
HydrateFallback: lazyHydrateFallbackForHydration,
589+
hydrateFallbackElement: lazyHydrateFallbackElementForHydration,
590+
loader: lazyLoaderForHydration,
591+
},
592+
},
593+
{
594+
path: "/navigation",
595+
lazy: {
596+
HydrateFallback: lazyHydrateFallbackForNavigation,
597+
hydrateFallbackElement: lazyHydrateFallbackElementForNavigation,
598+
loader: lazyLoaderForNavigation,
599+
},
600+
},
601+
],
602+
{
603+
initialEntries: ["/hydration"],
604+
}
605+
);
606+
expect(router.state.initialized).toBe(false);
607+
608+
expect(lazyHydrateFallbackForHydration).toHaveBeenCalledTimes(1);
609+
expect(lazyHydrateFallbackElementForHydration).toHaveBeenCalledTimes(1);
610+
expect(lazyLoaderForHydration).toHaveBeenCalledTimes(1);
611+
await lazyHydrateFallbackDeferredForHydration.resolve(null);
612+
await lazyHydrateFallbackElementDeferredForHydration.resolve(null);
613+
await lazyLoaderDeferredForHydration.resolve(null);
614+
615+
expect(router.state.location.pathname).toBe("/hydration");
616+
expect(router.state.navigation.state).toBe("idle");
617+
expect(router.state.initialized).toBe(true);
618+
619+
let navigationPromise = router.navigate("/navigation");
620+
expect(router.state.location.pathname).toBe("/hydration");
621+
expect(router.state.navigation.state).toBe("loading");
622+
expect(lazyHydrateFallbackForNavigation).not.toHaveBeenCalled();
623+
expect(lazyHydrateFallbackElementForNavigation).not.toHaveBeenCalled();
624+
expect(lazyLoaderForNavigation).toHaveBeenCalledTimes(1);
625+
await lazyLoaderDeferredForNavigation.resolve(null);
626+
await navigationPromise;
627+
expect(router.state.location.pathname).toBe("/navigation");
628+
expect(router.state.navigation.state).toBe("idle");
629+
});
630+
564631
it("fetches lazy route functions on fetcher.load", async () => {
565632
let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
566633
let t = setup({ routes });
@@ -606,6 +673,40 @@ describe("lazily loaded route modules", () => {
606673
expect(lazyLoader).toHaveBeenCalledTimes(1);
607674
});
608675

676+
it("skips lazy hydration route properties on fetcher.load", async () => {
677+
let [lazyLoader, lazyLoaderDeferred] = createAsyncStub();
678+
let lazyHydrateFallback = jest.fn(async () => null);
679+
let lazyHydrateFallbackElement = jest.fn(async () => null);
680+
let routes = createBasicLazyRoutes({
681+
loader: lazyLoader,
682+
// @ts-expect-error
683+
HydrateFallback: lazyHydrateFallback,
684+
hydrateFallbackElement: lazyHydrateFallbackElement,
685+
});
686+
let t = setup({ routes, hydrationRouteProperties });
687+
expect(lazyHydrateFallback).not.toHaveBeenCalled();
688+
expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
689+
690+
let key = "key";
691+
await t.fetch("/lazy", key);
692+
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
693+
expect(lazyLoader).toHaveBeenCalledTimes(1);
694+
expect(lazyHydrateFallback).not.toHaveBeenCalled();
695+
expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
696+
697+
let loaderDeferred = createDeferred();
698+
lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
699+
expect(t.router.state.fetchers.get(key)?.state).toBe("loading");
700+
701+
await loaderDeferred.resolve("LAZY LOADER");
702+
expect(t.fetchers[key].state).toBe("idle");
703+
expect(t.fetchers[key].data).toBe("LAZY LOADER");
704+
705+
expect(lazyLoader).toHaveBeenCalledTimes(1);
706+
expect(lazyHydrateFallback).not.toHaveBeenCalled();
707+
expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
708+
});
709+
609710
it("fetches lazy route functions on fetcher.submit", async () => {
610711
let { routes, lazy, lazyDeferred } = createBasicLazyFunctionRoutes();
611712
let t = setup({ routes });
@@ -666,6 +767,49 @@ describe("lazily loaded route modules", () => {
666767
expect(lazyAction).toHaveBeenCalledTimes(1);
667768
});
668769

770+
it("skips lazy hydration route properties on fetcher.submit", async () => {
771+
let [lazyLoaderStub, lazyLoaderDeferred] = createAsyncStub();
772+
let [lazyActionStub, lazyActionDeferred] = createAsyncStub();
773+
let lazyHydrateFallback = jest.fn(async () => null);
774+
let lazyHydrateFallbackElement = jest.fn(async () => null);
775+
let routes = createBasicLazyRoutes({
776+
loader: lazyLoaderStub,
777+
action: lazyActionStub,
778+
// @ts-expect-error
779+
HydrateFallback: lazyHydrateFallback,
780+
hydrateFallbackElement: lazyHydrateFallbackElement,
781+
});
782+
let t = setup({ routes, hydrationRouteProperties });
783+
expect(lazyLoaderStub).not.toHaveBeenCalled();
784+
expect(lazyActionStub).not.toHaveBeenCalled();
785+
786+
let key = "key";
787+
await t.fetch("/lazy", key, {
788+
formMethod: "post",
789+
formData: createFormData({}),
790+
});
791+
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
792+
expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
793+
expect(lazyActionStub).toHaveBeenCalledTimes(1);
794+
expect(lazyHydrateFallback).not.toHaveBeenCalled();
795+
expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
796+
797+
let actionDeferred = createDeferred();
798+
let loaderDeferred = createDeferred();
799+
lazyLoaderDeferred.resolve(() => loaderDeferred.promise);
800+
lazyActionDeferred.resolve(() => actionDeferred.promise);
801+
expect(t.router.state.fetchers.get(key)?.state).toBe("submitting");
802+
803+
await actionDeferred.resolve("LAZY ACTION");
804+
expect(t.fetchers[key]?.state).toBe("idle");
805+
expect(t.fetchers[key]?.data).toBe("LAZY ACTION");
806+
807+
expect(lazyLoaderStub).toHaveBeenCalledTimes(1);
808+
expect(lazyActionStub).toHaveBeenCalledTimes(1);
809+
expect(lazyHydrateFallback).not.toHaveBeenCalled();
810+
expect(lazyHydrateFallbackElement).not.toHaveBeenCalled();
811+
});
812+
669813
it("fetches lazy route functions on staticHandler.query()", async () => {
670814
let { query } = createStaticHandler([
671815
{
@@ -751,6 +895,35 @@ describe("lazily loaded route modules", () => {
751895
let data = await response.json();
752896
expect(data).toEqual({ value: "LAZY LOADER" });
753897
});
898+
899+
it("resolves lazy hydration route properties on staticHandler.queryRoute()", async () => {
900+
let lazyHydrateFallback = jest.fn(async () => null);
901+
let lazyHydrateFallbackElement = jest.fn(async () => null);
902+
let { queryRoute } = createStaticHandler(
903+
[
904+
{
905+
id: "lazy",
906+
path: "/lazy",
907+
lazy: {
908+
loader: async () => {
909+
await tick();
910+
return () => Response.json({ value: "LAZY LOADER" });
911+
},
912+
// @ts-expect-error
913+
HydrateFallback: lazyHydrateFallback,
914+
hydrateFallbackElement: lazyHydrateFallbackElement,
915+
},
916+
},
917+
],
918+
{ hydrationRouteProperties }
919+
);
920+
921+
let response = await queryRoute(createRequest("/lazy"));
922+
let data = await response.json();
923+
expect(data).toEqual({ value: "LAZY LOADER" });
924+
expect(lazyHydrateFallback).toHaveBeenCalled();
925+
expect(lazyHydrateFallbackElement).toHaveBeenCalled();
926+
});
754927
});
755928

756929
describe("statically defined fields", () => {

packages/react-router/__tests__/router/utils/data-router-setup.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ type SetupOpts = {
139139
basename?: string;
140140
initialEntries?: InitialEntry[];
141141
initialIndex?: number;
142+
hydrationRouteProperties?: string[];
142143
hydrationData?: HydrationState;
143144
dataStrategy?: DataStrategyFunction;
144145
};
@@ -204,6 +205,7 @@ export function setup({
204205
basename,
205206
initialEntries,
206207
initialIndex,
208+
hydrationRouteProperties,
207209
hydrationData,
208210
dataStrategy,
209211
}: SetupOpts) {
@@ -319,6 +321,7 @@ export function setup({
319321
basename,
320322
history,
321323
routes: enhanceRoutes(routes),
324+
hydrationRouteProperties,
322325
hydrationData,
323326
window: testWindow,
324327
dataStrategy: dataStrategy,

packages/react-router/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,10 @@ export {
317317
} from "./lib/context";
318318

319319
/** @internal */
320-
export { mapRouteProperties as UNSAFE_mapRouteProperties } from "./lib/components";
320+
export {
321+
hydrationRouteProperties as UNSAFE_hydrationRouteProperties,
322+
mapRouteProperties as UNSAFE_mapRouteProperties,
323+
} from "./lib/components";
321324

322325
/** @internal */
323326
export { FrameworkContext as UNSAFE_FrameworkContext } from "./lib/dom/ssr/components";

packages/react-router/lib/components.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ export function mapRouteProperties(route: RouteObject) {
131131
return updates;
132132
}
133133

134+
export const hydrationRouteProperties: (keyof RouteObject)[] = [
135+
"HydrateFallback",
136+
"hydrateFallbackElement",
137+
];
138+
134139
export interface MemoryRouterOpts {
135140
/**
136141
* Basename path for the application.
@@ -194,6 +199,7 @@ export function createMemoryRouter(
194199
}),
195200
hydrationData: opts?.hydrationData,
196201
routes,
202+
hydrationRouteProperties,
197203
mapRouteProperties,
198204
dataStrategy: opts?.dataStrategy,
199205
patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,

packages/react-router/lib/dom-export/hydrated-router.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
UNSAFE_shouldHydrateRouteLoader as shouldHydrateRouteLoader,
2222
UNSAFE_useFogOFWarDiscovery as useFogOFWarDiscovery,
2323
UNSAFE_mapRouteProperties as mapRouteProperties,
24+
UNSAFE_hydrationRouteProperties as hydrationRouteProperties,
2425
UNSAFE_createClientRoutesWithHMRRevalidationOptOut as createClientRoutesWithHMRRevalidationOptOut,
2526
matchRoutes,
2627
} from "react-router";
@@ -201,6 +202,7 @@ function createHydratedRouter({
201202
basename: ssrInfo.context.basename,
202203
unstable_getContext,
203204
hydrationData,
205+
hydrationRouteProperties,
204206
mapRouteProperties,
205207
future: {
206208
unstable_middleware: ssrInfo.context.future.unstable_middleware,

packages/react-router/lib/dom/lib.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ import {
6666
mergeRefs,
6767
usePrefetchBehavior,
6868
} from "./ssr/components";
69-
import { Router, mapRouteProperties } from "../components";
69+
import {
70+
Router,
71+
mapRouteProperties,
72+
hydrationRouteProperties,
73+
} from "../components";
7074
import type {
7175
RouteObject,
7276
NavigateOptions,
@@ -186,6 +190,7 @@ export function createBrowserRouter(
186190
hydrationData: opts?.hydrationData || parseHydrationData(),
187191
routes,
188192
mapRouteProperties,
193+
hydrationRouteProperties,
189194
dataStrategy: opts?.dataStrategy,
190195
patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,
191196
window: opts?.window,
@@ -209,6 +214,7 @@ export function createHashRouter(
209214
hydrationData: opts?.hydrationData || parseHydrationData(),
210215
routes,
211216
mapRouteProperties,
217+
hydrationRouteProperties,
212218
dataStrategy: opts?.dataStrategy,
213219
patchRoutesOnNavigation: opts?.patchRoutesOnNavigation,
214220
window: opts?.window,

0 commit comments

Comments
 (0)