Skip to content

Commit e89ad30

Browse files
authored
Run clientMiddleware on client navs if no loaders exist (#14106)
1 parent 49ea3c9 commit e89ad30

File tree

4 files changed

+218
-2
lines changed

4 files changed

+218
-2
lines changed

.changeset/thin-tables-reply.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
[UNSTABLE] Run client middleware on client navigations even if no loaders exist

integration/middleware-test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,79 @@ test.describe("Middleware", () => {
841841
appFixture.close();
842842
});
843843

844+
test("calls clientMiddleware when no loaders exist", async ({ page }) => {
845+
let fixture = await createFixture({
846+
files: {
847+
"react-router.config.ts": reactRouterConfig({
848+
middleware: true,
849+
}),
850+
"vite.config.ts": js`
851+
import { defineConfig } from "vite";
852+
import { reactRouter } from "@react-router/dev/vite";
853+
854+
export default defineConfig({
855+
build: { manifest: true, minify: false },
856+
plugins: [reactRouter()],
857+
});
858+
`,
859+
"app/routes/_index.tsx": js`
860+
import { Link } from 'react-router'
861+
862+
export const unstable_clientMiddleware = [
863+
({ context }) => {
864+
console.log('running index middleware')
865+
},
866+
];
867+
868+
export default function Component() {
869+
return (
870+
<>
871+
<h2 data-route>Index</h2>
872+
<Link to="/about">Go to about</Link>
873+
</>
874+
);
875+
}
876+
`,
877+
"app/routes/about.tsx": js`
878+
import { Link } from 'react-router'
879+
export const unstable_clientMiddleware = [
880+
({ context }) => {
881+
console.log('running about middleware')
882+
},
883+
];
884+
885+
export default function Component() {
886+
return (
887+
<>
888+
<h2 data-route>About</h2>
889+
<Link to="/">Go to index</Link>
890+
</>
891+
);
892+
}
893+
`,
894+
},
895+
});
896+
897+
let appFixture = await createAppFixture(fixture);
898+
899+
let logs: string[] = [];
900+
page.on("console", (msg) => logs.push(msg.text()));
901+
902+
let app = new PlaywrightFixture(appFixture, page);
903+
await app.goto("/");
904+
905+
(await page.$('a[href="/about"]'))?.click();
906+
await page.waitForSelector('[data-route]:has-text("About")');
907+
expect(logs).toEqual(["running about middleware"]);
908+
logs.splice(0);
909+
910+
(await page.$('a[href="/"]'))?.click();
911+
await page.waitForSelector('[data-route]:has-text("Index")');
912+
expect(logs).toEqual(["running index middleware"]);
913+
914+
appFixture.close();
915+
});
916+
844917
test("calls clientMiddleware before/after actions", async ({ page }) => {
845918
let fixture = await createFixture({
846919
files: {
@@ -1596,6 +1669,94 @@ test.describe("Middleware", () => {
15961669
appFixture.close();
15971670
});
15981671

1672+
test("calls middleware when no loaders exist on document, but not data requests", async ({
1673+
page,
1674+
}) => {
1675+
let oldConsoleLog = console.log;
1676+
let logs: any[] = [];
1677+
console.log = (...args) => logs.push(args);
1678+
1679+
let fixture = await createFixture({
1680+
files: {
1681+
"react-router.config.ts": reactRouterConfig({
1682+
middleware: true,
1683+
}),
1684+
"vite.config.ts": js`
1685+
import { defineConfig } from "vite";
1686+
import { reactRouter } from "@react-router/dev/vite";
1687+
1688+
export default defineConfig({
1689+
build: { manifest: true, minify: false },
1690+
plugins: [reactRouter()],
1691+
});
1692+
`,
1693+
"app/routes/parent.tsx": js`
1694+
import { Link, Outlet } from 'react-router'
1695+
1696+
export const unstable_middleware = [
1697+
({ request }) => {
1698+
console.log('Running parent middleware', new URL(request.url).pathname)
1699+
},
1700+
];
1701+
1702+
export default function Component() {
1703+
return (
1704+
<>
1705+
<h2>Parent</h2>
1706+
<Link to="/parent/a">Go to A</Link>
1707+
<Link to="/parent/b">Go to B</Link>
1708+
<Outlet/>
1709+
</>
1710+
);
1711+
}
1712+
`,
1713+
"app/routes/parent.a.tsx": js`
1714+
export const unstable_middleware = [
1715+
({ request }) => {
1716+
console.log('Running A middleware', new URL(request.url).pathname)
1717+
},
1718+
];
1719+
1720+
export default function Component() {
1721+
return <h3>A</h3>;
1722+
}
1723+
`,
1724+
"app/routes/parent.b.tsx": js`
1725+
export const unstable_middleware = [
1726+
({ request }) => {
1727+
console.log('Running B middleware', new URL(request.url).pathname)
1728+
},
1729+
];
1730+
1731+
export default function Component() {
1732+
return <h3>B</h3>;
1733+
}
1734+
`,
1735+
},
1736+
});
1737+
1738+
let appFixture = await createAppFixture(fixture);
1739+
1740+
let app = new PlaywrightFixture(appFixture, page);
1741+
await app.goto("/parent/a");
1742+
await page.waitForSelector('h2:has-text("Parent")');
1743+
await page.waitForSelector('h3:has-text("A")');
1744+
expect(logs).toEqual([
1745+
["Running parent middleware", "/parent/a"],
1746+
["Running A middleware", "/parent/a"],
1747+
]);
1748+
1749+
(await page.$('a[href="/parent/b"]'))?.click();
1750+
await page.waitForSelector('h3:has-text("B")');
1751+
expect(logs).toEqual([
1752+
["Running parent middleware", "/parent/a"],
1753+
["Running A middleware", "/parent/a"],
1754+
]);
1755+
1756+
appFixture.close();
1757+
console.log = oldConsoleLog;
1758+
});
1759+
15991760
test("calls middleware before/after actions", async ({ page }) => {
16001761
let fixture = await createFixture({
16011762
files: {

packages/react-router/__tests__/router/context-middleware-test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,54 @@ describe("context/middleware", () => {
290290
]);
291291
});
292292

293+
it("runs middleware even if no loaders exist", async () => {
294+
let snapshot;
295+
router = createRouter({
296+
history: createMemoryHistory(),
297+
routes: [
298+
{
299+
path: "/",
300+
},
301+
{
302+
id: "parent",
303+
path: "/parent",
304+
unstable_middleware: [
305+
async ({ context }, next) => {
306+
await next();
307+
// Grab a snapshot at the end of the upwards middleware chain
308+
snapshot = context.get(orderContext);
309+
},
310+
getOrderMiddleware(orderContext, "a"),
311+
getOrderMiddleware(orderContext, "b"),
312+
],
313+
children: [
314+
{
315+
id: "child",
316+
path: "child",
317+
unstable_middleware: [
318+
getOrderMiddleware(orderContext, "c"),
319+
getOrderMiddleware(orderContext, "d"),
320+
],
321+
},
322+
],
323+
},
324+
],
325+
});
326+
327+
await router.navigate("/parent/child");
328+
329+
expect(snapshot).toEqual([
330+
"a middleware - before next()",
331+
"b middleware - before next()",
332+
"c middleware - before next()",
333+
"d middleware - before next()",
334+
"d middleware - after next()",
335+
"c middleware - after next()",
336+
"b middleware - after next()",
337+
"a middleware - after next()",
338+
]);
339+
});
340+
293341
it("runs middleware sequentially before and after actions", async () => {
294342
let snapshot;
295343
router = createRouter({

packages/react-router/lib/router/router.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,11 +2046,13 @@ export function createRouter(init: RouterInit): Router {
20462046

20472047
pendingNavigationLoadId = ++incrementingLoadId;
20482048

2049-
// Short circuit if we have no loaders to run, unless there's a custom dataStrategy
2050-
// since they may have different revalidation rules (i.e., single fetch)
2049+
// Short circuit if we have no loaders/middlewares to run, unless there's a
2050+
// custom dataStrategy since they may have different revalidation rules
2051+
// (i.e., single fetch)
20512052
if (
20522053
!init.dataStrategy &&
20532054
!dsMatches.some((m) => m.shouldLoad) &&
2055+
!dsMatches.some((m) => m.route.unstable_middleware) &&
20542056
revalidatingFetchers.length === 0
20552057
) {
20562058
let updatedFetchers = markFetchRedirectsDone();

0 commit comments

Comments
 (0)