Skip to content

Commit 8186207

Browse files
authored
fix(rsc): preserve component metadata for client loader revalidation (#15323)
1 parent 971a499 commit 8186207

3 files changed

Lines changed: 88 additions & 23 deletions

File tree

integration/rsc/rsc-test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,11 @@ implementations.forEach((implementation) => {
485485
path: "client-route-component-props",
486486
lazy: () => import("./routes/client-route-component-props/home"),
487487
},
488+
{
489+
id: "client-loader-revalidation",
490+
path: "client-loader-revalidation",
491+
lazy: () => import("./routes/client-loader-revalidation/home"),
492+
},
488493
{
489494
id: "client-error-boundary-props-client-loader",
490495
path: "client-error-boundary-props-client-loader",
@@ -587,6 +592,17 @@ implementations.forEach((implementation) => {
587592
export default function RootRoute() {
588593
return <Outlet />;
589594
}
595+
596+
export { shouldRevalidate } from "./root.client";
597+
`,
598+
"src/routes/root.client.tsx": js`
599+
"use client";
600+
601+
export function shouldRevalidate({ currentUrl, defaultShouldRevalidate }) {
602+
return currentUrl.pathname === "/client-loader-revalidation"
603+
? false
604+
: defaultShouldRevalidate;
605+
}
590606
`,
591607

592608
"src/config/request-context.ts": js`
@@ -1213,6 +1229,36 @@ implementations.forEach((implementation) => {
12131229
}
12141230
`,
12151231

1232+
"src/routes/client-loader-revalidation/home.tsx": js`
1233+
export { default, clientLoader } from "./home.client";
1234+
`,
1235+
"src/routes/client-loader-revalidation/home.client.tsx": js`
1236+
"use client";
1237+
1238+
import { useRevalidator } from "react-router";
1239+
1240+
let count = 0;
1241+
1242+
export async function clientLoader() {
1243+
return { count: ++count };
1244+
}
1245+
1246+
export default function HomeRoute({ loaderData }) {
1247+
const revalidator = useRevalidator();
1248+
return (
1249+
<div>
1250+
<p data-client-loader-count>{loaderData.count}</p>
1251+
<button
1252+
data-revalidate
1253+
onClick={() => revalidator.revalidate()}
1254+
>
1255+
Revalidate
1256+
</button>
1257+
</div>
1258+
);
1259+
}
1260+
`,
1261+
12161262
"src/routes/client-error-boundary-props-client-loader/home.tsx": js`
12171263
export { default, clientLoader, ErrorBoundary } from "./home.client";
12181264
`,
@@ -2328,6 +2374,32 @@ implementations.forEach((implementation) => {
23282374
validateRSCHtml(await page.content());
23292375
});
23302376

2377+
test("Allows client loaders to opt out of server revalidation once their component is rendered", async ({
2378+
page,
2379+
}) => {
2380+
await page.goto(
2381+
`http://localhost:${port}/client-loader-revalidation`,
2382+
);
2383+
await expect(page.locator("[data-client-loader-count]")).toHaveText(
2384+
"1",
2385+
);
2386+
2387+
const rscRequests: string[] = [];
2388+
page.on("request", (request) => {
2389+
const url = new URL(request.url());
2390+
if (url.pathname.endsWith(".rsc")) {
2391+
rscRequests.push(url.href);
2392+
}
2393+
});
2394+
2395+
await page.click("[data-revalidate]");
2396+
await expect(page.locator("[data-client-loader-count]")).toHaveText(
2397+
"2",
2398+
);
2399+
2400+
expect(rscRequests).toEqual([]);
2401+
});
2402+
23312403
test("Passes props to client ErrorBoundary when error is thrown in client loader", async ({
23322404
page,
23332405
}) => {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Preserve RSC route component metadata so routes with a `clientLoader` can skip unnecessary server requests once their components have rendered while still fetching missing server-rendered elements.

packages/react-router/lib/rsc/browser.tsx

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -453,24 +453,26 @@ function createRouterFromPayload({
453453

454454
const renderedRoutesContext = createContext<RSCRouteManifest[]>();
455455

456+
type DataRouteObjectWithManifestInfo = DataRouteObject & {
457+
children?: DataRouteObjectWithManifestInfo[];
458+
hasLoader: boolean;
459+
hasClientLoader: boolean;
460+
hasComponent: boolean;
461+
hasAction: boolean;
462+
hasClientAction: boolean;
463+
};
464+
465+
type RSCDataRouteMatch = DataRouteMatch & {
466+
route: DataRouteObjectWithManifestInfo;
467+
};
468+
456469
export function getRSCSingleFetchDataStrategy(
457470
getRouter: () => DataRouter,
458471
ssr: boolean,
459472
createFromReadableStream: BrowserCreateFromReadableStreamFunction,
460473
fetchImplementation: (request: Request) => Promise<Response>,
461474
clientVersion?: string,
462475
): DataStrategyFunction {
463-
// TODO: Clean this up with a shared type
464-
type RSCDataRouteMatch = DataRouteMatch & {
465-
route: DataRouteObject & {
466-
hasLoader: boolean;
467-
hasClientLoader: boolean;
468-
hasComponent: boolean;
469-
hasAction: boolean;
470-
hasClientAction: boolean;
471-
};
472-
};
473-
474476
// create map
475477
let dataStrategy = getSingleFetchDataStrategyImpl(
476478
getRouter,
@@ -479,9 +481,6 @@ export function getRSCSingleFetchDataStrategy(
479481
return {
480482
hasLoader: M.route.hasLoader,
481483
hasClientLoader: M.route.hasClientLoader,
482-
hasComponent: M.route.hasComponent,
483-
hasAction: M.route.hasAction,
484-
hasClientAction: M.route.hasClientAction,
485484
};
486485
},
487486
// pass map into fetchAndDecode so it can add payloads
@@ -497,7 +496,7 @@ export function getRSCSingleFetchDataStrategy(
497496
// `serverLoader` or not, otherwise we'll have nothing to render.
498497
(match) => {
499498
let M = match as RSCDataRouteMatch;
500-
return M.route.hasComponent && !M.route.element;
499+
return !M.route.hasComponent || M.route.element != null;
501500
},
502501
);
503502
return async (args) =>
@@ -892,14 +891,6 @@ export function RSCHydratedRouter({
892891
);
893892
}
894893

895-
type DataRouteObjectWithManifestInfo = DataRouteObject & {
896-
children?: DataRouteObjectWithManifestInfo[];
897-
hasLoader: boolean;
898-
hasClientLoader: boolean;
899-
hasAction: boolean;
900-
hasClientAction: boolean;
901-
};
902-
903894
function createRouteFromServerManifest(
904895
match: RSCRouteManifest,
905896
payload?: RSCRenderPayload,
@@ -982,6 +973,7 @@ function createRouteFromServerManifest(
982973
// have a `loader` we may need to get the `element` implementation
983974
hasLoader: true,
984975
hasClientLoader: match.clientLoader != null,
976+
hasComponent: match.hasComponent,
985977
hasAction: match.hasAction,
986978
hasClientAction: match.clientAction != null,
987979
};

0 commit comments

Comments
 (0)