Skip to content

Commit bb21567

Browse files
authored
Middleware code cleanup (#12884)
1 parent beaa4f5 commit bb21567

File tree

4 files changed

+834
-756
lines changed

4 files changed

+834
-756
lines changed

integration/middleware-test.ts

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,301 @@ test.describe("Middleware", () => {
899899

900900
appFixture.close();
901901
});
902+
903+
test("calls clientMiddleware once when multiple server requests happen", async ({
904+
page,
905+
}) => {
906+
let fixture = await createFixture({
907+
files: {
908+
"react-router.config.ts": reactRouterConfig({}),
909+
"vite.config.ts": js`
910+
import { defineConfig } from "vite";
911+
import { reactRouter } from "@react-router/dev/vite";
912+
913+
export default defineConfig({
914+
build: { manifest: true, minify: false },
915+
plugins: [reactRouter()],
916+
});
917+
`,
918+
"app/entry.client.tsx": js`
919+
import { HydratedRouter } from "react-router/dom";
920+
import { startTransition, StrictMode } from "react";
921+
import { hydrateRoot } from "react-dom/client";
922+
923+
startTransition(() => {
924+
hydrateRoot(
925+
document,
926+
<StrictMode>
927+
<HydratedRouter context={{
928+
parent: { value: 0 },
929+
child: { value: 0 }
930+
}} />
931+
</StrictMode>
932+
);
933+
});
934+
`,
935+
"app/routes/_index.tsx": js`
936+
import { Link } from 'react-router'
937+
export default function Component({ loaderData }) {
938+
return <Link to="/parent/child">Go to /parent/child</Link>;
939+
}
940+
`,
941+
"app/routes/parent.tsx": js`
942+
import { Outlet } from 'react-router';
943+
export function loader() {
944+
return 'PARENT'
945+
}
946+
export const clientMiddleware = [
947+
({ context }) => { context.parent.value++ },
948+
];
949+
950+
export async function clientLoader({ serverLoader, context }) {
951+
return {
952+
serverData: await serverLoader(),
953+
context
954+
}
955+
}
956+
957+
export default function Component({ loaderData }) {
958+
return (
959+
<>
960+
<h2 data-parent>{JSON.stringify(loaderData)}</h2>
961+
<Outlet/>
962+
</>
963+
);
964+
}
965+
`,
966+
"app/routes/parent.child.tsx": js`
967+
export function loader() {
968+
return 'CHILD'
969+
}
970+
export const clientMiddleware = [
971+
({ context }) => { context.child.value++ },
972+
];
973+
974+
export async function clientLoader({ serverLoader, context }) {
975+
return {
976+
serverData: await serverLoader(),
977+
context
978+
}
979+
}
980+
981+
export default function Component({ loaderData }) {
982+
return <h3 data-child>{JSON.stringify(loaderData)}</h3>;
983+
}
984+
`,
985+
},
986+
});
987+
988+
let appFixture = await createAppFixture(fixture);
989+
990+
let requests: string[] = [];
991+
page.on("request", (request: PlaywrightRequest) => {
992+
if (request.url().includes(".data")) {
993+
requests.push(request.url());
994+
}
995+
});
996+
997+
let app = new PlaywrightFixture(appFixture, page);
998+
await app.goto("/");
999+
(await page.$('a[href="/parent/child"]'))?.click();
1000+
await page.waitForSelector("[data-child]");
1001+
1002+
// 2 separate server requests made
1003+
expect(requests).toEqual([
1004+
expect.stringContaining("/parent/child.data?_routes=routes%2Fparent"),
1005+
expect.stringContaining(
1006+
"/parent/child.data?_routes=routes%2Fparent.child"
1007+
),
1008+
]);
1009+
1010+
// But client middlewares only ran once
1011+
let json = (await page.locator("[data-parent]").textContent()) as string;
1012+
expect(JSON.parse(json)).toEqual({
1013+
serverData: "PARENT",
1014+
context: {
1015+
parent: { value: 1 },
1016+
child: { value: 1 },
1017+
},
1018+
});
1019+
json = (await page.locator("[data-child]").textContent()) as string;
1020+
expect(JSON.parse(json)).toEqual({
1021+
serverData: "CHILD",
1022+
context: {
1023+
parent: { value: 1 },
1024+
child: { value: 1 },
1025+
},
1026+
});
1027+
1028+
appFixture.close();
1029+
});
1030+
1031+
test("calls clientMiddleware once when multiple server requests happen and some routes opt out", async ({
1032+
page,
1033+
}) => {
1034+
let fixture = await createFixture({
1035+
files: {
1036+
"react-router.config.ts": reactRouterConfig({}),
1037+
"vite.config.ts": js`
1038+
import { defineConfig } from "vite";
1039+
import { reactRouter } from "@react-router/dev/vite";
1040+
1041+
export default defineConfig({
1042+
build: { manifest: true, minify: false },
1043+
plugins: [reactRouter()],
1044+
});
1045+
`,
1046+
"app/entry.client.tsx": js`
1047+
import { HydratedRouter } from "react-router/dom";
1048+
import { startTransition, StrictMode } from "react";
1049+
import { hydrateRoot } from "react-dom/client";
1050+
1051+
startTransition(() => {
1052+
hydrateRoot(
1053+
document,
1054+
<StrictMode>
1055+
<HydratedRouter context={{
1056+
parent: { value: 0 },
1057+
child: { value: 0 },
1058+
index: { value: 0 }
1059+
}} />
1060+
</StrictMode>
1061+
);
1062+
});
1063+
`,
1064+
"app/routes/_index.tsx": js`
1065+
import { Link } from 'react-router'
1066+
export default function Component({ loaderData }) {
1067+
return <Link to="/parent/child">Go to /parent/child</Link>;
1068+
}
1069+
`,
1070+
"app/routes/parent.tsx": js`
1071+
import { Outlet } from 'react-router';
1072+
export function loader() {
1073+
return 'PARENT'
1074+
}
1075+
export const clientMiddleware = [
1076+
({ context }) => { context.parent.value++ },
1077+
];
1078+
export default function Component({ loaderData }) {
1079+
return (
1080+
<>
1081+
<h2 data-parent>{loaderData}</h2>
1082+
<Outlet/>
1083+
</>
1084+
);
1085+
}
1086+
export function shouldRevalidate() {
1087+
return false;
1088+
}
1089+
`,
1090+
"app/routes/parent.child.tsx": js`
1091+
import { Outlet } from 'react-router';
1092+
export function loader() {
1093+
return 'CHILD'
1094+
}
1095+
export const clientMiddleware = [
1096+
({ context }) => { context.child.value++ },
1097+
];
1098+
export default function Component({ loaderData }) {
1099+
return (
1100+
<>
1101+
<h3 data-child>{loaderData}</h3>
1102+
<Outlet/>
1103+
</>
1104+
);
1105+
}
1106+
`,
1107+
"app/routes/parent.child._index.tsx": js`
1108+
import { Form } from 'react-router';
1109+
export function action() {
1110+
return 'INDEX ACTION'
1111+
}
1112+
export function loader() {
1113+
return 'INDEX'
1114+
}
1115+
export const clientMiddleware = [
1116+
({ context }) => { context.index.value++ },
1117+
];
1118+
export async function clientLoader({ serverLoader, context }) {
1119+
return {
1120+
serverData: await serverLoader(),
1121+
context
1122+
}
1123+
}
1124+
export default function Component({ loaderData, actionData }) {
1125+
return (
1126+
<>
1127+
<h4 data-index>{JSON.stringify(loaderData)}</h4>
1128+
<Form method="post">
1129+
<button type="submit">Submit</button>
1130+
</Form>
1131+
{actionData ? <p data-action>{JSON.stringify(actionData)}</p> : null}
1132+
</>
1133+
);
1134+
}
1135+
`,
1136+
},
1137+
});
1138+
1139+
let appFixture = await createAppFixture(fixture);
1140+
1141+
let requests: string[] = [];
1142+
page.on("request", (request: PlaywrightRequest) => {
1143+
if (request.method() === "GET" && request.url().includes(".data")) {
1144+
requests.push(request.url());
1145+
}
1146+
});
1147+
1148+
let app = new PlaywrightFixture(appFixture, page);
1149+
await app.goto("/");
1150+
(await page.$('a[href="/parent/child"]'))?.click();
1151+
await page.waitForSelector("[data-child]");
1152+
expect(await page.locator("[data-parent]").textContent()).toBe("PARENT");
1153+
expect(await page.locator("[data-child]").textContent()).toBe("CHILD");
1154+
expect(
1155+
JSON.parse((await page.locator("[data-index]").textContent())!)
1156+
).toEqual({
1157+
serverData: "INDEX",
1158+
context: {
1159+
parent: { value: 1 },
1160+
child: { value: 1 },
1161+
index: { value: 1 },
1162+
},
1163+
});
1164+
1165+
requests = []; // clear before form submission
1166+
(await page.$('button[type="submit"]'))?.click();
1167+
await page.waitForSelector("[data-action]");
1168+
1169+
// 2 separate server requests made
1170+
expect(requests).toEqual([
1171+
// index gets it's own due to clientLoader
1172+
expect.stringMatching(
1173+
/\/parent\/child\.data\?_routes=routes%2Fparent\.child\._index$/
1174+
),
1175+
// This is the normal request but only included parent.child because parent opted out
1176+
expect.stringMatching(
1177+
/\/parent\/child\.data\?_routes=routes%2Fparent\.child$/
1178+
),
1179+
]);
1180+
1181+
// But client middlewares only ran once for the action and once for the revalidation
1182+
expect(await page.locator("[data-parent]").textContent()).toBe("PARENT");
1183+
expect(await page.locator("[data-child]").textContent()).toBe("CHILD");
1184+
expect(
1185+
JSON.parse((await page.locator("[data-index]").textContent())!)
1186+
).toEqual({
1187+
serverData: "INDEX",
1188+
context: {
1189+
parent: { value: 3 },
1190+
child: { value: 3 },
1191+
index: { value: 3 },
1192+
},
1193+
});
1194+
1195+
appFixture.close();
1196+
});
9021197
});
9031198

9041199
test.describe("Server Middleware", () => {
@@ -1529,6 +1824,79 @@ test.describe("Middleware", () => {
15291824
appFixture.close();
15301825
});
15311826

1827+
test("bubbles errors on the way down up to at least the highest route with a loader", async ({
1828+
page,
1829+
}) => {
1830+
let fixture = await createFixture(
1831+
{
1832+
files: {
1833+
"vite.config.ts": js`
1834+
import { defineConfig } from "vite";
1835+
import { reactRouter } from "@react-router/dev/vite";
1836+
1837+
export default defineConfig({
1838+
build: { manifest: true, minify: false },
1839+
plugins: [reactRouter()],
1840+
});
1841+
`,
1842+
"app/routes/_index.tsx": js`
1843+
import { Link } from 'react-router'
1844+
export default function Component({ loaderData }) {
1845+
return <Link to="/a/b">Link</Link>
1846+
}
1847+
`,
1848+
"app/routes/a.tsx": js`
1849+
import { Outlet } from 'react-router'
1850+
export default function Component() {
1851+
return <Outlet/>
1852+
}
1853+
export function ErrorBoundary({ error }) {
1854+
return <><h1>A Error Boundary</h1><pre>{error.message}</pre></>
1855+
}
1856+
`,
1857+
"app/routes/a.b.tsx": js`
1858+
import { Outlet } from 'react-router'
1859+
export function loader() {
1860+
return null;
1861+
}
1862+
export default function Component() {
1863+
return <Outlet/>
1864+
}
1865+
`,
1866+
"app/routes/a.b.c.tsx": js`
1867+
import { Outlet } from 'react-router'
1868+
export default function Component() {
1869+
return <Outlet/>
1870+
}
1871+
export function ErrorBoundary({ error }) {
1872+
return <><h1>C Error Boundary</h1><pre>{error.message}</pre></>
1873+
}
1874+
`,
1875+
"app/routes/a.b.c.d.tsx": js`
1876+
import { Outlet } from 'react-router'
1877+
export const middleware = [() => { throw new Error("broken!") }]
1878+
export default function Component() {
1879+
return <Outlet/>
1880+
}
1881+
`,
1882+
},
1883+
},
1884+
UNSAFE_ServerMode.Development
1885+
);
1886+
1887+
let appFixture = await createAppFixture(
1888+
fixture,
1889+
UNSAFE_ServerMode.Development
1890+
);
1891+
1892+
let app = new PlaywrightFixture(appFixture, page);
1893+
await app.goto("/a/b/c/d");
1894+
expect(await page.locator("h1").textContent()).toBe("A Error Boundary");
1895+
expect(await page.locator("pre").textContent()).toBe("broken!");
1896+
1897+
appFixture.close();
1898+
});
1899+
15321900
test("only calls middleware as deep as needed for granular data requests", async ({
15331901
page,
15341902
}) => {

0 commit comments

Comments
 (0)