Skip to content

Commit 597f3d2

Browse files
authored
Fix clientLoader calls to serverLoader for prerendered routes (#13047)
1 parent 3066205 commit 597f3d2

File tree

3 files changed

+131
-95
lines changed

3 files changed

+131
-95
lines changed

.changeset/orange-balloons-bathe.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+
[REMOVE] Fix prerender calls to serverLoader from clientLoader

integration/vite-prerender-test.ts

Lines changed: 106 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import { PassThrough } from "node:stream";
4+
import type { Page } from "@playwright/test";
45
import { test, expect } from "@playwright/test";
56

67
import {
@@ -784,6 +785,20 @@ test.describe("Prerendering", () => {
784785
});
785786

786787
test.describe("ssr: false", () => {
788+
function captureRequests(page: Page) {
789+
let requests: string[] = [];
790+
page.on("request", (request) => {
791+
let url = new URL(request.url());
792+
if (
793+
url.pathname.endsWith(".data") ||
794+
url.pathname.endsWith("__manifest")
795+
) {
796+
requests.push(url.pathname + url.search);
797+
}
798+
});
799+
return requests;
800+
}
801+
787802
test("Errors on headers/action functions in any route", async () => {
788803
let cwd = await createProject({
789804
"react-router.config.ts": reactRouterConfig({
@@ -979,14 +994,7 @@ test.describe("Prerendering", () => {
979994
});
980995
appFixture = await createAppFixture(fixture);
981996

982-
let requests: string[] = [];
983-
page.on("request", (request) => {
984-
let pathname = new URL(request.url()).pathname;
985-
if (pathname.endsWith(".data") || pathname.endsWith("__manifest")) {
986-
requests.push(pathname);
987-
}
988-
});
989-
997+
let requests = captureRequests(page);
990998
let app = new PlaywrightFixture(appFixture, page);
991999
await app.goto("/");
9921000
await page.waitForSelector("[data-mounted]");
@@ -1048,7 +1056,7 @@ test.describe("Prerendering", () => {
10481056
);
10491057
});
10501058

1051-
test("Properly navigates across SPA/prerender pages when starting from a SPA page", async ({
1059+
test("Navigates across SPA/prerender pages when starting from a SPA page", async ({
10521060
page,
10531061
}) => {
10541062
fixture = await createFixture({
@@ -1138,14 +1146,7 @@ test.describe("Prerendering", () => {
11381146
});
11391147
appFixture = await createAppFixture(fixture);
11401148

1141-
let requests: string[] = [];
1142-
page.on("request", (request) => {
1143-
let pathname = new URL(request.url()).pathname;
1144-
if (pathname.endsWith(".data") || pathname.endsWith("__manifest")) {
1145-
requests.push(pathname);
1146-
}
1147-
});
1148-
1149+
let requests = captureRequests(page);
11491150
let app = new PlaywrightFixture(appFixture, page);
11501151
await app.goto("/", true);
11511152
await page.waitForSelector('a[href="/page"]');
@@ -1194,7 +1195,7 @@ test.describe("Prerendering", () => {
11941195
expect(requests).toEqual(["/page.data", "/page.data"]);
11951196
});
11961197

1197-
test("Properly navigates across SPA/prerender pages when starting from a prerendered page", async ({
1198+
test("Navigates across SPA/prerender pages when starting from a prerendered page", async ({
11981199
page,
11991200
}) => {
12001201
fixture = await createFixture({
@@ -1284,14 +1285,7 @@ test.describe("Prerendering", () => {
12841285
});
12851286
appFixture = await createAppFixture(fixture);
12861287

1287-
let requests: string[] = [];
1288-
page.on("request", (request) => {
1289-
let pathname = new URL(request.url()).pathname;
1290-
if (pathname.endsWith(".data") || pathname.endsWith("__manifest")) {
1291-
requests.push(pathname);
1292-
}
1293-
});
1294-
1288+
let requests = captureRequests(page);
12951289
let app = new PlaywrightFixture(appFixture, page);
12961290
await app.goto("/", true);
12971291
await page.waitForSelector('a[href="/page"]');
@@ -1340,7 +1334,7 @@ test.describe("Prerendering", () => {
13401334
expect(requests).toEqual(["/page.data", "/page.data"]);
13411335
});
13421336

1343-
test("Properly navigates across SPA/prerender pages when starting from a SPA page and a root loader exists", async ({
1337+
test("Navigates across SPA/prerender pages when starting from a SPA page and a root loader exists", async ({
13441338
page,
13451339
}) => {
13461340
fixture = await createFixture({
@@ -1439,14 +1433,7 @@ test.describe("Prerendering", () => {
14391433
});
14401434
appFixture = await createAppFixture(fixture);
14411435

1442-
let requests: string[] = [];
1443-
page.on("request", (request) => {
1444-
let pathname = new URL(request.url()).pathname;
1445-
if (pathname.endsWith(".data") || pathname.endsWith("__manifest")) {
1446-
requests.push(pathname);
1447-
}
1448-
});
1449-
1436+
let requests = captureRequests(page);
14501437
let app = new PlaywrightFixture(appFixture, page);
14511438
await app.goto("/", true);
14521439
await page.waitForSelector("[data-root]");
@@ -1498,7 +1485,7 @@ test.describe("Prerendering", () => {
14981485
expect(requests).toEqual(["/page.data", "/page.data"]);
14991486
});
15001487

1501-
test("Properly navigates across SPA/prerender pages when starting from a prerendered page and a root loader exists", async ({
1488+
test("Navigates across SPA/prerender pages when starting from a prerendered page and a root loader exists", async ({
15021489
page,
15031490
}) => {
15041491
fixture = await createFixture({
@@ -1597,14 +1584,7 @@ test.describe("Prerendering", () => {
15971584
});
15981585
appFixture = await createAppFixture(fixture);
15991586

1600-
let requests: string[] = [];
1601-
page.on("request", (request) => {
1602-
let pathname = new URL(request.url()).pathname;
1603-
if (pathname.endsWith(".data") || pathname.endsWith("__manifest")) {
1604-
requests.push(pathname);
1605-
}
1606-
});
1607-
1587+
let requests = captureRequests(page);
16081588
let app = new PlaywrightFixture(appFixture, page);
16091589
await app.goto("/", true);
16101590
await page.waitForSelector("[data-root]");
@@ -1656,7 +1636,7 @@ test.describe("Prerendering", () => {
16561636
expect(requests).toEqual(["/page.data", "/page.data"]);
16571637
});
16581638

1659-
test("Properly navigates between prerendered parent and child SPA route", async ({
1639+
test("Navigates between prerendered parent and child SPA route", async ({
16601640
page,
16611641
}) => {
16621642
fixture = await createFixture({
@@ -1743,14 +1723,7 @@ test.describe("Prerendering", () => {
17431723
});
17441724
appFixture = await createAppFixture(fixture);
17451725

1746-
let requests: string[] = [];
1747-
page.on("request", (request) => {
1748-
let pathname = new URL(request.url()).pathname;
1749-
if (pathname.endsWith(".data") || pathname.endsWith("__manifest")) {
1750-
requests.push(pathname);
1751-
}
1752-
});
1753-
1726+
let requests = captureRequests(page);
17541727
let app = new PlaywrightFixture(appFixture, page);
17551728
await app.goto("/parent", true);
17561729
await expect(page.getByText("PARENT DATA")).toBeVisible();
@@ -1796,7 +1769,7 @@ test.describe("Prerendering", () => {
17961769
expect(requests).toEqual([]);
17971770
});
17981771

1799-
test("Properly navigates between SPA parent and prerendered child route", async ({
1772+
test("Navigates between SPA parent and prerendered child route", async ({
18001773
page,
18011774
}) => {
18021775
fixture = await createFixture({
@@ -1880,14 +1853,7 @@ test.describe("Prerendering", () => {
18801853
});
18811854
appFixture = await createAppFixture(fixture);
18821855

1883-
let requests: string[] = [];
1884-
page.on("request", (request) => {
1885-
let pathname = new URL(request.url()).pathname;
1886-
if (pathname.endsWith(".data") || pathname.endsWith("__manifest")) {
1887-
requests.push(pathname);
1888-
}
1889-
});
1890-
1856+
let requests = captureRequests(page);
18911857
let app = new PlaywrightFixture(appFixture, page);
18921858
await app.goto("/parent", true);
18931859
await expect(page.getByText("PARENT DATA")).toBeVisible();
@@ -1911,7 +1877,7 @@ test.describe("Prerendering", () => {
19111877

19121878
// Initial navigation and submission from /parent
19131879
expect(requests).toEqual(["/parent/child.data", "/parent/child.data"]);
1914-
requests = [];
1880+
while (requests.length) requests.pop();
19151881

19161882
await app.goto("/parent/child", true);
19171883
await expect(page.getByText("PARENT DATA")).toBeVisible();
@@ -1935,7 +1901,7 @@ test.describe("Prerendering", () => {
19351901
expect(requests).toEqual(["/parent/child.data"]);
19361902
});
19371903

1938-
test("Properly navigates between prerendered parent and child SPA route (with a root loader)", async ({
1904+
test("Navigates between prerendered parent and child SPA route (with a root loader)", async ({
19391905
page,
19401906
}) => {
19411907
fixture = await createFixture({
@@ -2035,14 +2001,7 @@ test.describe("Prerendering", () => {
20352001
});
20362002
appFixture = await createAppFixture(fixture);
20372003

2038-
let requests: string[] = [];
2039-
page.on("request", (request) => {
2040-
let pathname = new URL(request.url()).pathname;
2041-
if (pathname.endsWith(".data") || pathname.endsWith("__manifest")) {
2042-
requests.push(pathname);
2043-
}
2044-
});
2045-
2004+
let requests = captureRequests(page);
20462005
let app = new PlaywrightFixture(appFixture, page);
20472006
await app.goto("/parent", true);
20482007
await expect(page.getByText("ROOT DATA")).toBeVisible();
@@ -2090,7 +2049,7 @@ test.describe("Prerendering", () => {
20902049
expect(requests).toEqual([]);
20912050
});
20922051

2093-
test("Properly navigates between SPA parent and prerendered child route (with a root loader)", async ({
2052+
test("Navigates between SPA parent and prerendered child route (with a root loader)", async ({
20942053
page,
20952054
}) => {
20962055
fixture = await createFixture({
@@ -2183,14 +2142,7 @@ test.describe("Prerendering", () => {
21832142
});
21842143
appFixture = await createAppFixture(fixture);
21852144

2186-
let requests: string[] = [];
2187-
page.on("request", (request) => {
2188-
let pathname = new URL(request.url()).pathname;
2189-
if (pathname.endsWith(".data") || pathname.endsWith("__manifest")) {
2190-
requests.push(pathname);
2191-
}
2192-
});
2193-
2145+
let requests = captureRequests(page);
21942146
let app = new PlaywrightFixture(appFixture, page);
21952147
await app.goto("/parent", true);
21962148
await expect(page.getByText("ROOT DATA")).toBeVisible();
@@ -2215,7 +2167,7 @@ test.describe("Prerendering", () => {
22152167

22162168
// Initial navigation and submission from /parent
22172169
expect(requests).toEqual(["/parent/child.data", "/parent/child.data"]);
2218-
requests = [];
2170+
while (requests.length) requests.pop();
22192171

22202172
await app.goto("/parent/child", true);
22212173
await expect(page.getByText("PARENT DATA")).toBeVisible();
@@ -2239,6 +2191,77 @@ test.describe("Prerendering", () => {
22392191
expect(requests).toEqual(["/parent/child.data"]);
22402192
});
22412193

2194+
test("Navigates to prerendered parent with clientLoader calling loader", async ({
2195+
page,
2196+
}) => {
2197+
fixture = await createFixture({
2198+
prerender: true,
2199+
files: {
2200+
"react-router.config.ts": reactRouterConfig({
2201+
ssr: false,
2202+
prerender: ["/", "/parent"],
2203+
}),
2204+
"vite.config.ts": files["vite.config.ts"],
2205+
"app/root.tsx": js`
2206+
import * as React from "react";
2207+
import { Link, Outlet, Scripts } from "react-router";
2208+
2209+
export function Layout({ children }) {
2210+
return (
2211+
<html lang="en">
2212+
<head />
2213+
<body>
2214+
{children}
2215+
<Scripts />
2216+
</body>
2217+
</html>
2218+
);
2219+
}
2220+
2221+
export default function Root({ loaderData }) {
2222+
return (
2223+
<>
2224+
<Link to="/parent">Go to parent</Link>
2225+
<Outlet/>
2226+
</>
2227+
);
2228+
}
2229+
2230+
export function HydrateFallback() {
2231+
return <p>Loading...</p>;
2232+
}
2233+
`,
2234+
"app/routes/parent.tsx": js`
2235+
import { Link, Form, Outlet } from 'react-router';
2236+
export async function loader() {
2237+
return "PARENT DATA"
2238+
}
2239+
export async function clientLoader({ serverLoader }) {
2240+
let str = await serverLoader();
2241+
return str + " - CLIENT"
2242+
}
2243+
export function clientAction() {
2244+
return "PARENT ACTION"
2245+
}
2246+
export default function Parent({ loaderData, actionData }) {
2247+
return <p data-parent>{loaderData}</p>;
2248+
}
2249+
`,
2250+
},
2251+
});
2252+
appFixture = await createAppFixture(fixture);
2253+
2254+
let requests = captureRequests(page);
2255+
let app = new PlaywrightFixture(appFixture, page);
2256+
await app.goto("/", true);
2257+
await expect(page.getByText("Go to parent")).toBeVisible();
2258+
2259+
await app.clickLink("/parent");
2260+
await expect(page.getByText("PARENT DATA - CLIENT")).toBeVisible();
2261+
2262+
expect(requests).toEqual(["/parent.data?_routes=routes%2Fparent"]);
2263+
});
2264+
22422265
test("Handles 404s on data requests", async ({ page }) => {
22432266
fixture = await createFixture({
22442267
prerender: true,
@@ -2266,14 +2289,7 @@ test.describe("Prerendering", () => {
22662289
});
22672290
appFixture = await createAppFixture(fixture);
22682291

2269-
let requests: string[] = [];
2270-
page.on("request", (request) => {
2271-
let pathname = new URL(request.url()).pathname;
2272-
if (pathname.endsWith(".data")) {
2273-
requests.push(pathname);
2274-
}
2275-
});
2276-
2292+
let requests = captureRequests(page);
22772293
let app = new PlaywrightFixture(appFixture, page);
22782294
await app.goto("/");
22792295
await page.waitForSelector("[data-mounted]");

packages/react-router/lib/dom/ssr/single-fetch.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,27 @@ export function getSingleFetchDataStrategy(
186186
// Skip single fetch and just call the loaders in parallel when this is
187187
// a SPA mode navigation
188188
let matchesToLoad = matches.filter((m) => m.shouldLoad);
189-
let results = await Promise.all(matchesToLoad.map((m) => m.resolve()));
190-
return results.reduce(
191-
(acc, result, i) =>
192-
Object.assign(acc, { [matchesToLoad[i].route.id]: result }),
193-
{}
189+
let url = stripIndexParam(singleFetchUrl(request.url));
190+
let init = await createRequestInit(request);
191+
let results: Record<string, DataStrategyResult> = {};
192+
await Promise.all(
193+
matchesToLoad.map((m) =>
194+
m.resolve(async (handler) => {
195+
try {
196+
// Need to pass through a `singleFetch` override handler so
197+
// clientLoader's can still call server loaders through `.data`
198+
// requests
199+
let result = manifest.routes[m.route.id]?.hasClientLoader
200+
? await fetchSingleLoader(handler, url, init, m.route.id)
201+
: await handler();
202+
results[m.route.id] = { type: "data", result };
203+
} catch (e) {
204+
results[m.route.id] = { type: "error", result: e };
205+
}
206+
})
207+
)
194208
);
209+
return results;
195210
}
196211
}
197212

0 commit comments

Comments
 (0)