Skip to content

Commit 91c7bac

Browse files
Add client middleware to split route modules (#13210)
1 parent bc43707 commit 91c7bac

File tree

17 files changed

+751
-29
lines changed

17 files changed

+751
-29
lines changed

.changeset/large-shoes-live.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": patch
3+
---
4+
5+
When both `future.unstable_middleware` and `future.unstable_splitRouteModules` are enabled, split `unstable_clientMiddleware` route exports into separate chunks when possible

.changeset/silent-apples-return.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Add support for `route.unstable_lazyMiddleware` function to allow lazy loading of middleware logic.
6+
7+
**Breaking change for `unstable_middleware` consumers**
8+
9+
The `route.unstable_middleware` property is no longer supported in the return value from `route.lazy`. If you want to lazily load middleware, you must use `route.unstable_lazyMiddleware`.

.changeset/strong-countries-tap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": patch
3+
---
4+
5+
Improve performance of `future.unstable_middleware` by ensuring that route modules are only blocking during the middleware phase when the `unstable_clientMiddleware` has been defined

integration/middleware-test.ts

Lines changed: 185 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,97 @@ test.describe("Middleware", () => {
113113
appFixture.close();
114114
});
115115

116+
test("calls clientMiddleware before/after loaders with split route modules", async ({
117+
page,
118+
}) => {
119+
let fixture = await createFixture({
120+
spaMode: true,
121+
files: {
122+
"react-router.config.ts": reactRouterConfig({
123+
ssr: false,
124+
middleware: true,
125+
splitRouteModules: true,
126+
}),
127+
"vite.config.ts": js`
128+
import { defineConfig } from "vite";
129+
import { reactRouter } from "@react-router/dev/vite";
130+
131+
export default defineConfig({
132+
build: { manifest: true, minify: false },
133+
plugins: [reactRouter()],
134+
});
135+
`,
136+
"app/context.ts": js`
137+
import { unstable_createContext } from 'react-router'
138+
export const orderContext = unstable_createContext([]);
139+
`,
140+
"app/routes/_index.tsx": js`
141+
import { Link } from 'react-router'
142+
import { orderContext } from '../context'
143+
144+
export const unstable_clientMiddleware = [
145+
({ context }) => {
146+
context.set(orderContext, [...context.get(orderContext), 'a']);
147+
},
148+
({ context }) => {
149+
context.set(orderContext, [...context.get(orderContext), 'b']);
150+
},
151+
];
152+
153+
export async function clientLoader({ request, context }) {
154+
return context.get(orderContext).join(',');
155+
}
156+
157+
export default function Component({ loaderData }) {
158+
return (
159+
<>
160+
<h2 data-route>Index: {loaderData}</h2>
161+
<Link to="/about">Go to about</Link>
162+
</>
163+
);
164+
}
165+
`,
166+
"app/routes/about.tsx": js`
167+
import { orderContext } from '../context'
168+
169+
export const unstable_clientMiddleware = [
170+
({ context }) => {
171+
context.set(orderContext, [...context.get(orderContext), 'c']);
172+
},
173+
({ context }) => {
174+
context.set(orderContext, [...context.get(orderContext), 'd']);
175+
},
176+
];
177+
178+
export async function clientLoader({ context }) {
179+
return context.get(orderContext).join(',');
180+
}
181+
182+
export default function Component({ loaderData }) {
183+
return <h2 data-route>About: {loaderData}</h2>;
184+
}
185+
`,
186+
},
187+
});
188+
189+
let appFixture = await createAppFixture(fixture);
190+
191+
let app = new PlaywrightFixture(appFixture, page);
192+
await app.goto("/");
193+
await page.waitForSelector('[data-route]:has-text("Index")');
194+
expect(await page.locator("[data-route]").textContent()).toBe(
195+
"Index: a,b"
196+
);
197+
198+
(await page.$('a[href="/about"]'))?.click();
199+
await page.waitForSelector('[data-route]:has-text("About")');
200+
expect(await page.locator("[data-route]").textContent()).toBe(
201+
"About: c,d"
202+
);
203+
204+
appFixture.close();
205+
});
206+
116207
test("calls clientMiddleware before/after actions", async ({ page }) => {
117208
let fixture = await createFixture({
118209
spaMode: true,
@@ -596,6 +687,94 @@ test.describe("Middleware", () => {
596687
appFixture.close();
597688
});
598689

690+
test("calls clientMiddleware before/after loaders with split route modules", async ({
691+
page,
692+
}) => {
693+
let fixture = await createFixture({
694+
files: {
695+
"react-router.config.ts": reactRouterConfig({
696+
middleware: true,
697+
splitRouteModules: true,
698+
}),
699+
"vite.config.ts": js`
700+
import { defineConfig } from "vite";
701+
import { reactRouter } from "@react-router/dev/vite";
702+
703+
export default defineConfig({
704+
build: { manifest: true, minify: false },
705+
plugins: [reactRouter()],
706+
});
707+
`,
708+
"app/context.ts": js`
709+
import { unstable_createContext } from 'react-router'
710+
export const orderContext = unstable_createContext([]);
711+
`,
712+
"app/routes/_index.tsx": js`
713+
import { Link } from 'react-router'
714+
import { orderContext } from "../context";;
715+
716+
export const unstable_clientMiddleware = [
717+
({ context }) => {
718+
context.set(orderContext, [...context.get(orderContext), 'a']);
719+
},
720+
({ context }) => {
721+
context.set(orderContext, [...context.get(orderContext), 'b']);
722+
},
723+
];
724+
725+
export async function clientLoader({ request, context }) {
726+
return context.get(orderContext).join(',');
727+
}
728+
729+
export default function Component({ loaderData }) {
730+
return (
731+
<>
732+
<h2 data-route>Index: {loaderData}</h2>
733+
<Link to="/about">Go to about</Link>
734+
</>
735+
);
736+
}
737+
`,
738+
"app/routes/about.tsx": js`
739+
import { orderContext } from "../context";;
740+
export const unstable_clientMiddleware = [
741+
({ context }) => {
742+
context.set(orderContext, ['c']); // reset order from hydration
743+
},
744+
({ context }) => {
745+
context.set(orderContext, [...context.get(orderContext), 'd']);
746+
},
747+
];
748+
749+
export async function clientLoader({ context }) {
750+
return context.get(orderContext).join(',');
751+
}
752+
753+
export default function Component({ loaderData }) {
754+
return <h2 data-route>About: {loaderData}</h2>;
755+
}
756+
`,
757+
},
758+
});
759+
760+
let appFixture = await createAppFixture(fixture);
761+
762+
let app = new PlaywrightFixture(appFixture, page);
763+
await app.goto("/");
764+
await page.waitForSelector('[data-route]:has-text("Index")');
765+
expect(await page.locator("[data-route]").textContent()).toBe(
766+
"Index: a,b"
767+
);
768+
769+
(await page.$('a[href="/about"]'))?.click();
770+
await page.waitForSelector('[data-route]:has-text("About")');
771+
expect(await page.locator("[data-route]").textContent()).toBe(
772+
"About: c,d"
773+
);
774+
775+
appFixture.close();
776+
});
777+
599778
test("calls clientMiddleware before/after actions", async ({ page }) => {
600779
let fixture = await createFixture({
601780
files: {
@@ -1074,7 +1253,7 @@ test.describe("Middleware", () => {
10741253
await page.waitForSelector("[data-child]");
10751254

10761255
// 2 separate server requests made
1077-
expect(requests).toEqual([
1256+
expect(requests.sort()).toEqual([
10781257
expect.stringContaining("/parent/child.data?_routes=routes%2Fparent"),
10791258
expect.stringContaining(
10801259
"/parent/child.data?_routes=routes%2Fparent.child"
@@ -1236,15 +1415,15 @@ test.describe("Middleware", () => {
12361415
await page.waitForSelector("[data-action]");
12371416

12381417
// 2 separate server requests made
1239-
expect(requests).toEqual([
1240-
// index gets it's own due to clientLoader
1241-
expect.stringMatching(
1242-
/\/parent\/child\.data\?_routes=routes%2Fparent\.child\._index$/
1243-
),
1418+
expect(requests.sort()).toEqual([
12441419
// This is the normal request but only included parent.child because parent opted out
12451420
expect.stringMatching(
12461421
/\/parent\/child\.data\?_routes=routes%2Fparent\.child$/
12471422
),
1423+
// index gets it's own due to clientLoader
1424+
expect.stringMatching(
1425+
/\/parent\/child\.data\?_routes=routes%2Fparent\.child\._index$/
1426+
),
12481427
]);
12491428

12501429
// But client middlewares only ran once for the action and once for the revalidation

packages/react-router-dev/manifest.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ export type ManifestRoute = {
77
module: string;
88
clientLoaderModule: string | undefined;
99
clientActionModule: string | undefined;
10+
clientMiddlewareModule: string | undefined;
1011
hydrateFallbackModule: string | undefined;
1112
imports?: string[];
1213
hasAction: boolean;
1314
hasLoader: boolean;
1415
hasClientAction: boolean;
1516
hasClientLoader: boolean;
17+
hasClientMiddleware: boolean;
1618
hasErrorBoundary: boolean;
1719
};
1820

packages/react-router-dev/vite/plugin.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
843843
let isRootRoute = route.parentId === undefined;
844844
let hasClientAction = sourceExports.includes("clientAction");
845845
let hasClientLoader = sourceExports.includes("clientLoader");
846+
let hasClientMiddleware = sourceExports.includes(
847+
"unstable_clientMiddleware"
848+
);
846849
let hasHydrateFallback = sourceExports.includes("HydrateFallback");
847850

848851
let { hasRouteChunkByExportName } = await detectRouteChunksIfEnabled(
@@ -861,6 +864,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
861864
!hasClientAction || hasRouteChunkByExportName.clientAction,
862865
clientLoader:
863866
!hasClientLoader || hasRouteChunkByExportName.clientLoader,
867+
unstable_clientMiddleware:
868+
!hasClientMiddleware ||
869+
hasRouteChunkByExportName.unstable_clientMiddleware,
864870
HydrateFallback:
865871
!hasHydrateFallback || hasRouteChunkByExportName.HydrateFallback,
866872
},
@@ -877,6 +883,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
877883
hasLoader: sourceExports.includes("loader"),
878884
hasClientAction,
879885
hasClientLoader,
886+
hasClientMiddleware,
880887
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
881888
...getReactRouterManifestBuildAssets(
882889
ctx,
@@ -901,6 +908,14 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
901908
getRouteChunkModuleId(routeFile, "clientLoader")
902909
)
903910
: undefined,
911+
clientMiddlewareModule:
912+
hasRouteChunkByExportName.unstable_clientMiddleware
913+
? getPublicModulePathForEntry(
914+
ctx,
915+
viteManifest,
916+
getRouteChunkModuleId(routeFile, "unstable_clientMiddleware")
917+
)
918+
: undefined,
904919
hydrateFallbackModule: hasRouteChunkByExportName.HydrateFallback
905920
? getPublicModulePathForEntry(
906921
ctx,
@@ -971,6 +986,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
971986
let sourceExports = routeManifestExports[key];
972987
let hasClientAction = sourceExports.includes("clientAction");
973988
let hasClientLoader = sourceExports.includes("clientLoader");
989+
let hasClientMiddleware = sourceExports.includes(
990+
"unstable_clientMiddleware"
991+
);
974992
let hasHydrateFallback = sourceExports.includes("HydrateFallback");
975993
let routeModulePath = combineURLs(
976994
ctx.publicPath,
@@ -996,6 +1014,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
9961014
!hasClientAction || hasRouteChunkByExportName.clientAction,
9971015
clientLoader:
9981016
!hasClientLoader || hasRouteChunkByExportName.clientLoader,
1017+
unstable_clientMiddleware:
1018+
!hasClientMiddleware ||
1019+
hasRouteChunkByExportName.unstable_clientMiddleware,
9991020
HydrateFallback:
10001021
!hasHydrateFallback || hasRouteChunkByExportName.HydrateFallback,
10011022
},
@@ -1012,11 +1033,13 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
10121033
// Split route modules are a build-time optimization
10131034
clientActionModule: undefined,
10141035
clientLoaderModule: undefined,
1036+
clientMiddlewareModule: undefined,
10151037
hydrateFallbackModule: undefined,
10161038
hasAction: sourceExports.includes("action"),
10171039
hasLoader: sourceExports.includes("loader"),
10181040
hasClientAction,
10191041
hasClientLoader,
1042+
hasClientMiddleware,
10201043
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
10211044
imports: [],
10221045
};
@@ -1885,6 +1908,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
18851908
valid: {
18861909
clientAction: !exportNames.includes("clientAction"),
18871910
clientLoader: !exportNames.includes("clientLoader"),
1911+
unstable_clientMiddleware: !exportNames.includes(
1912+
"unstable_clientMiddleware"
1913+
),
18881914
HydrateFallback: !exportNames.includes("HydrateFallback"),
18891915
},
18901916
});
@@ -2220,6 +2246,8 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
22202246
"hasAction",
22212247
"hasClientAction",
22222248
"clientActionModule",
2249+
"hasClientMiddleware",
2250+
"clientMiddlewareModule",
22232251
"hasErrorBoundary",
22242252
"hydrateFallbackModule",
22252253
] as const
@@ -2423,13 +2451,17 @@ async function getRouteMetadata(
24232451
clientLoaderModule: hasRouteChunkByExportName.clientLoader
24242452
? `${getRouteChunkModuleId(moduleUrl, "clientLoader")}`
24252453
: undefined,
2454+
clientMiddlewareModule: hasRouteChunkByExportName.unstable_clientMiddleware
2455+
? `${getRouteChunkModuleId(moduleUrl, "unstable_clientMiddleware")}`
2456+
: undefined,
24262457
hydrateFallbackModule: hasRouteChunkByExportName.HydrateFallback
24272458
? `${getRouteChunkModuleId(moduleUrl, "HydrateFallback")}`
24282459
: undefined,
24292460
hasAction: sourceExports.includes("action"),
24302461
hasClientAction: sourceExports.includes("clientAction"),
24312462
hasLoader: sourceExports.includes("loader"),
24322463
hasClientLoader: sourceExports.includes("clientLoader"),
2464+
hasClientMiddleware: sourceExports.includes("unstable_clientMiddleware"),
24332465
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
24342466
imports: [],
24352467
};
@@ -3076,6 +3108,7 @@ async function detectRouteChunksIfEnabled(
30763108
hasRouteChunkByExportName: {
30773109
clientAction: false,
30783110
clientLoader: false,
3111+
unstable_clientMiddleware: false,
30793112
HydrateFallback: false,
30803113
},
30813114
};
@@ -3438,7 +3471,10 @@ export async function getEnvironmentOptionsResolvers(
34383471
entryFileNames: ({ moduleIds }) => {
34393472
let routeChunkModuleId = moduleIds.find(isRouteChunkModuleId);
34403473
let routeChunkName = routeChunkModuleId
3441-
? getRouteChunkNameFromModuleId(routeChunkModuleId)
3474+
? getRouteChunkNameFromModuleId(routeChunkModuleId)?.replace(
3475+
"unstable_",
3476+
""
3477+
)
34423478
: null;
34433479
let routeChunkSuffix = routeChunkName
34443480
? `-${kebabCase(routeChunkName)}`

0 commit comments

Comments
 (0)