Skip to content

Commit 98641e2

Browse files
authored
feat: rsc prefetch (#14902)
1 parent b16b549 commit 98641e2

File tree

4 files changed

+91
-4
lines changed

4 files changed

+91
-4
lines changed

.changeset/slimy-tips-explode.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+
rsc Link prefetch

packages/react-router/lib/dom/ssr/components.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export function Links({ nonce, crossOrigin }: LinksProps): React.JSX.Element {
343343
* tags
344344
*/
345345
export function PrefetchPageLinks({ page, ...linkProps }: PageLinkDescriptor) {
346+
let rsc = useIsRSCRouterContext();
346347
let { router } = useDataRouterContext();
347348
let matches = React.useMemo(
348349
() => matchRoutes(router.routes, page, router.basename),
@@ -353,6 +354,12 @@ export function PrefetchPageLinks({ page, ...linkProps }: PageLinkDescriptor) {
353354
return null;
354355
}
355356

357+
if (rsc) {
358+
return (
359+
<RSCPrefetchPageLinksImpl page={page} matches={matches} {...linkProps} />
360+
);
361+
}
362+
356363
return <PrefetchPageLinksImpl page={page} matches={matches} {...linkProps} />;
357364
}
358365

@@ -382,6 +389,62 @@ function useKeyedPrefetchLinks(matches: DataRouteMatch[]) {
382389
return keyedPrefetchLinks;
383390
}
384391

392+
function RSCPrefetchPageLinksImpl({
393+
page,
394+
matches: nextMatches,
395+
...linkProps
396+
}: PageLinkDescriptor & {
397+
matches: DataRouteMatch[];
398+
}) {
399+
let location = useLocation();
400+
let { future } = useFrameworkContext();
401+
let { basename } = useDataRouterContext();
402+
403+
let dataHrefs = React.useMemo(() => {
404+
if (page === location.pathname + location.search + location.hash) {
405+
// Because we opt-into revalidation, don't compute this for the current page
406+
// since it would always trigger a prefetch of the existing loaders
407+
return [];
408+
}
409+
let url = singleFetchUrl(
410+
page,
411+
basename,
412+
future.unstable_trailingSlashAwareDataRequests,
413+
"rsc",
414+
);
415+
416+
let hasSomeRoutesWithShouldRevalidate = false;
417+
let targetRoutes: string[] = [];
418+
for (let match of nextMatches) {
419+
if (typeof match.route.shouldRevalidate === "function") {
420+
hasSomeRoutesWithShouldRevalidate = true;
421+
} else {
422+
targetRoutes.push(match.route.id);
423+
}
424+
}
425+
426+
if (hasSomeRoutesWithShouldRevalidate && targetRoutes.length > 0) {
427+
url.searchParams.set("_routes", targetRoutes.join(","));
428+
}
429+
430+
return [url.pathname + url.search];
431+
}, [
432+
basename,
433+
future.unstable_trailingSlashAwareDataRequests,
434+
page,
435+
location,
436+
nextMatches,
437+
]);
438+
439+
return (
440+
<>
441+
{dataHrefs.map((href) => (
442+
<link key={href} rel="prefetch" as="fetch" href={href} {...linkProps} />
443+
))}
444+
</>
445+
);
446+
}
447+
385448
function PrefetchPageLinksImpl({
386449
page,
387450
matches: nextMatches,

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,6 @@ async function generateManifestResponse(
559559
statusCode: 200,
560560
headers: new Headers({
561561
"Content-Type": "text/x-component",
562-
Vary: "Content-Type",
563562
}),
564563
payload,
565564
},
@@ -977,7 +976,6 @@ function generateRedirectResponse(
977976
// https://nodejs.org/api/http.html#class-httpclientrequest
978977
headers.delete("Content-Length");
979978
headers.set("Content-Type", "text/x-component");
980-
headers.set("Vary", "Content-Type");
981979

982980
return generateResponse(
983981
{

playground/rsc-vite-framework/app/root.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
import type { Route } from "./+types/root";
22

3-
import { Meta, Link, Outlet, isRouteErrorResponse } from "react-router";
3+
import { Meta, Link, Outlet, isRouteErrorResponse, type MiddlewareFunction } from "react-router";
44
import "./root.css";
55

6+
export const middleware: MiddlewareFunction[] = [
7+
async ({ request }, next) => {
8+
let response = await next();
9+
10+
if (
11+
request.method === "GET" &&
12+
response instanceof Response &&
13+
response.status === 200 &&
14+
request.headers.get("sec-purpose") === "prefetch" &&
15+
!response.headers.has("Cache-Control")
16+
) {
17+
let cachedResponse = new Response(response.body, response);
18+
cachedResponse.headers.set("Cache-Control", "max-age=5");
19+
return cachedResponse;
20+
}
21+
return response;
22+
}
23+
];
24+
625
export const meta = () => [{ title: "React Router Vite" }];
726

27+
export const shouldRevalidate = () => false;
28+
829
export function Layout({ children }: { children: React.ReactNode }) {
930
console.log("Layout");
1031
return (
@@ -23,7 +44,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
2344
<Link to="/">Home</Link>
2445
</li>
2546
<li>
26-
<Link to="/server-loader">Server loader</Link>
47+
<Link to="/server-loader" prefetch="intent">Server loader</Link>
2748
</li>
2849
<li>
2950
<Link to="/client-loader">Client loader</Link>

0 commit comments

Comments
 (0)