Skip to content

Commit c843029

Browse files
authored
Adjust approach for prerendering/SPA mode via headers (#13453)
1 parent 69b6e05 commit c843029

File tree

7 files changed

+95
-31
lines changed

7 files changed

+95
-31
lines changed

.changeset/stale-bats-swim.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@react-router/dev": patch
3+
"react-router": patch
4+
---
5+
6+
Adjust approach for Prerendering/SPA Mode via headers

integration/vite-prerender-test.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ test.describe("Prerendering", () => {
602602
"app/routes/about.tsx": js`
603603
import { useLoaderData } from 'react-router';
604604
export function loader({ request }) {
605-
return "ABOUT-" + request.headers.has('X-React-Router-Prerender');
605+
return "ABOUT-" + Boolean(process.env.IS_RR_BUILD_REQUEST);
606606
}
607607
608608
export default function Comp() {
@@ -613,7 +613,7 @@ test.describe("Prerendering", () => {
613613
"app/routes/not-prerendered.tsx": js`
614614
import { useLoaderData } from 'react-router';
615615
export function loader({ request }) {
616-
return "NOT-PRERENDERED-" + request.headers.has('X-React-Router-Prerender');
616+
return "NOT-PRERENDERED-" + Boolean(process.env.IS_RR_BUILD_REQUEST);
617617
}
618618
619619
export default function Comp() {
@@ -659,7 +659,7 @@ test.describe("Prerendering", () => {
659659
import { useLoaderData } from 'react-router';
660660
export function loader({ request }) {
661661
return {
662-
prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
662+
prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no",
663663
// 24999 characters
664664
data: new Array(5000).fill('test').join('-'),
665665
};
@@ -712,7 +712,7 @@ test.describe("Prerendering", () => {
712712
import { useLoaderData } from 'react-router';
713713
export function loader({ request }) {
714714
return {
715-
prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
715+
prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no",
716716
data: "한글 데이터 - UTF-8 문자",
717717
};
718718
}
@@ -732,7 +732,7 @@ test.describe("Prerendering", () => {
732732
import { useLoaderData } from 'react-router';
733733
export function loader({ request }) {
734734
return {
735-
prerendered: request.headers.has('X-React-Router-Prerender') ? 'yes' : 'no',
735+
prerendered: process.env.IS_RR_BUILD_REQUEST ?? "no",
736736
data: "非プリレンダリングデータ - UTF-8文字",
737737
};
738738
}
@@ -837,6 +837,18 @@ test.describe("Prerendering", () => {
837837
await page.waitForSelector("[data-mounted]");
838838
expect(await app.getHtml()).toMatch("Index: INDEX");
839839
});
840+
841+
test("Ignores build-time headers at runtime", async () => {
842+
fixture = await createFixture({ files });
843+
let res = await fixture.requestSingleFetchData("/_root.data", {
844+
headers: {
845+
"X-React-Router-Prerender-Data": encodeURI(
846+
'[{"_1":2},"routes/_index",{"_3":4},"data","Hello World!"]'
847+
),
848+
},
849+
});
850+
expect((res.data as any)["routes/_index"].data).toBe("Index Loader Data");
851+
});
840852
});
841853

842854
test.describe("ssr: false", () => {

integration/vite-spa-mode-test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,37 @@ test.describe("SPA Mode", () => {
234234
expect(await res.text()).toMatch(/^<!DOCTYPE html><html lang="en">/);
235235
});
236236

237+
test("Ignores build-time headers at runtime", async () => {
238+
let fixture = await createFixture({
239+
files: {
240+
"react-router.config.ts": reactRouterConfig({
241+
splitRouteModules,
242+
}),
243+
"app/root.tsx": js`
244+
import { Outlet, Scripts } from "react-router";
245+
246+
export default function Root() {
247+
return (
248+
<html lang="en">
249+
<head></head>
250+
<body>
251+
<h1 data-root>Root</h1>
252+
<Scripts />
253+
</body>
254+
</html>
255+
);
256+
}
257+
`,
258+
},
259+
});
260+
let res = await fixture.requestDocument("/", {
261+
headers: { "X-React-Router-SPA-Mode": "yes" },
262+
});
263+
let html = await res.text();
264+
expect(html).toMatch('"isSpaMode":false');
265+
expect(html).toMatch('<h1 data-root="true">Root</h1>');
266+
});
267+
237268
test("works when combined with a basename", async ({ page }) => {
238269
fixture = await createFixture({
239270
spaMode: true,

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

+11-16
Original file line numberDiff line numberDiff line change
@@ -1724,6 +1724,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
17241724
);
17251725
}
17261726

1727+
// Set an environment variable we can look for in the handler to
1728+
// enable some build-time-only logic
1729+
process.env.IS_RR_BUILD_REQUEST = "yes";
1730+
17271731
if (isPrerenderingEnabled(ctx.reactRouterConfig)) {
17281732
// If we have prerender routes, that takes precedence over SPA mode
17291733
// which is ssr:false and only the root route being rendered
@@ -2623,11 +2627,6 @@ async function handlePrerender(
26232627
}
26242628

26252629
let buildRoutes = createPrerenderRoutes(build.routes);
2626-
let headers = {
2627-
// Header that can be used in the loader to know if you're running at
2628-
// build time or runtime
2629-
"X-React-Router-Prerender": "yes",
2630-
};
26312630
for (let path of build.prerender) {
26322631
// Ensure we have a leading slash for matching
26332632
let matches = matchRoutes(buildRoutes, `/${path}/`.replace(/^\/\/+/, "/"));
@@ -2655,17 +2654,15 @@ async function handlePrerender(
26552654
[leafRoute.id],
26562655
clientBuildDirectory,
26572656
reactRouterConfig,
2658-
viteConfig,
2659-
{ headers }
2657+
viteConfig
26602658
);
26612659
// Prerender a raw file for external consumption
26622660
await prerenderResourceRoute(
26632661
handler,
26642662
path,
26652663
clientBuildDirectory,
26662664
reactRouterConfig,
2667-
viteConfig,
2668-
{ headers }
2665+
viteConfig
26692666
);
26702667
} else {
26712668
viteConfig.logger.warn(
@@ -2684,8 +2681,7 @@ async function handlePrerender(
26842681
null,
26852682
clientBuildDirectory,
26862683
reactRouterConfig,
2687-
viteConfig,
2688-
{ headers }
2684+
viteConfig
26892685
);
26902686
}
26912687

@@ -2698,11 +2694,10 @@ async function handlePrerender(
26982694
data
26992695
? {
27002696
headers: {
2701-
...headers,
27022697
"X-React-Router-Prerender-Data": encodeURI(data),
27032698
},
27042699
}
2705-
: { headers }
2700+
: undefined
27062701
);
27072702
}
27082703
}
@@ -2746,7 +2741,7 @@ async function prerenderData(
27462741
clientBuildDirectory: string,
27472742
reactRouterConfig: ResolvedReactRouterConfig,
27482743
viteConfig: Vite.ResolvedConfig,
2749-
requestInit: RequestInit
2744+
requestInit?: RequestInit
27502745
) {
27512746
let normalizedPath = `${reactRouterConfig.basename}${
27522747
prerenderPath === "/"
@@ -2789,7 +2784,7 @@ async function prerenderRoute(
27892784
clientBuildDirectory: string,
27902785
reactRouterConfig: ResolvedReactRouterConfig,
27912786
viteConfig: Vite.ResolvedConfig,
2792-
requestInit: RequestInit
2787+
requestInit?: RequestInit
27932788
) {
27942789
let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`.replace(
27952790
/\/\/+/g,
@@ -2845,7 +2840,7 @@ async function prerenderResourceRoute(
28452840
clientBuildDirectory: string,
28462841
reactRouterConfig: ResolvedReactRouterConfig,
28472842
viteConfig: Vite.ResolvedConfig,
2848-
requestInit: RequestInit
2843+
requestInit?: RequestInit
28492844
) {
28502845
let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`
28512846
.replace(/\/\/+/g, "/")

packages/react-router/lib/server-runtime/dev.ts

+12
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,15 @@ export function getDevServerHooks(): DevServerHooks | undefined {
1414
// @ts-expect-error
1515
return globalThis[globalDevServerHooksKey];
1616
}
17+
18+
// Guarded access to build-time-only headers
19+
export function getBuildTimeHeader(request: Request, headerName: string) {
20+
if (typeof process !== "undefined") {
21+
try {
22+
if (process.env?.IS_RR_BUILD_REQUEST === "yes") {
23+
return request.headers.get(headerName);
24+
}
25+
} catch (e) {}
26+
}
27+
return null;
28+
}

packages/react-router/lib/server-runtime/routes.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from "../dom/ssr/single-fetch";
2020
import invariant from "./invariant";
2121
import type { ServerRouteModule } from "../dom/ssr/routeModules";
22+
import { getBuildTimeHeader } from "./dev";
2223

2324
export type ServerRouteManifest = RouteManifest<Omit<ServerRoute, "children">>;
2425

@@ -86,10 +87,11 @@ export function createStaticHandlerDataRoutes(
8687
? async (args: RRLoaderFunctionArgs) => {
8788
// If we're prerendering, use the data passed in from prerendering
8889
// the .data route so we don't call loaders twice
89-
if (args.request.headers.has("X-React-Router-Prerender-Data")) {
90-
const preRenderedData = args.request.headers.get(
91-
"X-React-Router-Prerender-Data"
92-
);
90+
let preRenderedData = getBuildTimeHeader(
91+
args.request,
92+
"X-React-Router-Prerender-Data"
93+
);
94+
if (preRenderedData != null) {
9395
let encoded = preRenderedData
9496
? decodeURI(preRenderedData)
9597
: preRenderedData;

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

+12-6
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { matchServerRoutes } from "./routeMatching";
2323
import type { ServerRoute } from "./routes";
2424
import { createStaticHandlerDataRoutes, createRoutes } from "./routes";
2525
import { createServerHandoffString } from "./serverHandoff";
26-
import { getDevServerHooks } from "./dev";
26+
import { getBuildTimeHeader, getDevServerHooks } from "./dev";
2727
import {
2828
encodeViaTurboStream,
2929
getSingleFetchRedirect,
@@ -164,12 +164,17 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
164164
normalizedPath = normalizedPath.slice(0, -1);
165165
}
166166

167+
let isSpaMode =
168+
getBuildTimeHeader(request, "X-React-Router-SPA-Mode") === "yes";
169+
167170
// When runtime SSR is disabled, make our dev server behave like the deployed
168171
// pre-rendered site would
169172
if (!_build.ssr) {
173+
// When SSR is disabled this, file can only ever run during dev because we
174+
// delete the server build at the end of the build
170175
if (_build.prerender.length === 0) {
171-
// Add the header if we're in SPA mode
172-
request.headers.set("X-React-Router-SPA-Mode", "yes");
176+
// ssr:false and no prerender config indicates "SPA Mode"
177+
isSpaMode = true;
173178
} else if (
174179
!_build.prerender.includes(normalizedPath) &&
175180
!_build.prerender.includes(normalizedPath + "/")
@@ -194,7 +199,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
194199
});
195200
} else {
196201
// Serve a SPA fallback for non-pre-rendered document requests
197-
request.headers.set("X-React-Router-SPA-Mode", "yes");
202+
isSpaMode = true;
198203
}
199204
}
200205
}
@@ -275,7 +280,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
275280
}
276281
}
277282
} else if (
278-
!request.headers.has("X-React-Router-SPA-Mode") &&
283+
!isSpaMode &&
279284
matches &&
280285
matches[matches.length - 1].route.module.default == null &&
281286
matches[matches.length - 1].route.module.ErrorBoundary == null
@@ -309,6 +314,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
309314
request,
310315
loadContext,
311316
handleError,
317+
isSpaMode,
312318
criticalCss
313319
);
314320
}
@@ -426,9 +432,9 @@ async function handleDocumentRequest(
426432
request: Request,
427433
loadContext: AppLoadContext | unstable_RouterContextProvider,
428434
handleError: (err: unknown) => void,
435+
isSpaMode: boolean,
429436
criticalCss?: CriticalCss
430437
) {
431-
let isSpaMode = request.headers.has("X-React-Router-SPA-Mode");
432438
try {
433439
let response = await staticHandler.query(request, {
434440
requestContext: loadContext,

0 commit comments

Comments
 (0)