Skip to content

Commit a354db5

Browse files
authored
Fix shoulRevalidate behavior in clientLoader-only routes (#13221)
1 parent 99551ae commit a354db5

File tree

3 files changed

+107
-18
lines changed

3 files changed

+107
-18
lines changed

.changeset/three-oranges-sort.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+
Fix `shouldRevalidate` behavior for `clientLoader`-only routes in `ssr:true` apps

integration/single-fetch-test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2200,6 +2200,97 @@ test.describe("single-fetch", () => {
22002200
).toBe(true);
22012201
});
22022202

2203+
test("allows reused routes to opt out via shouldRevalidate (w/only clientLoader)", async ({
2204+
page,
2205+
}) => {
2206+
let fixture = await createFixture({
2207+
files: {
2208+
...files,
2209+
"app/routes/_index.tsx": js`
2210+
import { Link } from "react-router";
2211+
export default function Component() {
2212+
return <Link to="/parent/a">Go to /parent/a</Link>;
2213+
}
2214+
`,
2215+
"app/routes/parent.tsx": js`
2216+
import { Link, Outlet, useLoaderData } from "react-router";
2217+
let count = 0;
2218+
export function clientLoader({ request }) {
2219+
return { count: ++count };
2220+
}
2221+
export function shouldRevalidate() {
2222+
return false;
2223+
}
2224+
export default function Component() {
2225+
return (
2226+
<>
2227+
<p id="parent">Parent Count: {useLoaderData().count}</p>
2228+
<Link to="/parent/a">Go to /parent/a</Link>
2229+
<Link to="/parent/b">Go to /parent/b</Link>
2230+
<Outlet/>
2231+
</>
2232+
);
2233+
}
2234+
`,
2235+
"app/routes/parent.a.tsx": js`
2236+
import { useLoaderData } from "react-router";
2237+
let count = 0;
2238+
export function loader({ request }) {
2239+
return { count: ++count };
2240+
}
2241+
export default function Component() {
2242+
return <p id="a">A Count: {useLoaderData().count}</p>;
2243+
}
2244+
`,
2245+
"app/routes/parent.b.tsx": js`
2246+
import { useLoaderData } from "react-router";
2247+
let count = 0;
2248+
export function loader({ request }) {
2249+
return { count: ++count };
2250+
}
2251+
export default function Component() {
2252+
return <p id="b">B Count: {useLoaderData().count}</p>;
2253+
}
2254+
`,
2255+
},
2256+
});
2257+
let appFixture = await createAppFixture(fixture);
2258+
let app = new PlaywrightFixture(appFixture, page);
2259+
2260+
let urls: string[] = [];
2261+
page.on("request", (req) => {
2262+
if (req.url().includes(".data")) {
2263+
urls.push(req.url());
2264+
}
2265+
});
2266+
2267+
await app.goto("/");
2268+
2269+
await app.clickLink("/parent/a");
2270+
await page.waitForSelector("#a");
2271+
expect(await app.getHtml("#parent")).toContain("Parent Count: 1");
2272+
expect(await app.getHtml("#a")).toContain("A Count: 1");
2273+
expect(urls.length).toBe(1);
2274+
// Client loader triggers 2 requests on the first navigation
2275+
expect(urls[0].endsWith("/parent/a.data")).toBe(true);
2276+
urls = [];
2277+
2278+
await app.clickLink("/parent/b");
2279+
await page.waitForSelector("#b");
2280+
expect(await app.getHtml("#parent")).toContain("Parent Count: 1");
2281+
expect(await app.getHtml("#b")).toContain("B Count: 1");
2282+
expect(urls.length).toBe(1);
2283+
expect(urls[0].endsWith("/parent/b.data")).toBe(true);
2284+
urls = [];
2285+
2286+
await app.clickLink("/parent/a");
2287+
await page.waitForSelector("#a");
2288+
expect(await app.getHtml("#parent")).toContain("Parent Count: 1");
2289+
expect(await app.getHtml("#a")).toContain("A Count: 2");
2290+
expect(urls.length).toBe(1);
2291+
expect(urls[0].endsWith("/parent/a.data")).toBe(true);
2292+
});
2293+
22032294
test("provides the proper defaultShouldRevalidate value", async ({
22042295
page,
22052296
}) => {

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

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -305,21 +305,6 @@ async function nonSsrStrategy(
305305
return results;
306306
}
307307

308-
function isOptedOut(
309-
manifestRoute: EntryRoute | undefined,
310-
routeModule: RouteModule | undefined,
311-
match: DataStrategyMatch,
312-
router: DataRouter
313-
) {
314-
return (
315-
match.route.id in router.state.loaderData &&
316-
manifestRoute &&
317-
manifestRoute.hasLoader &&
318-
routeModule &&
319-
routeModule.shouldRevalidate
320-
);
321-
}
322-
323308
// Loaders are trickier since we only want to hit the server once, so we
324309
// create a singular promise for all server-loader routes to latch onto.
325310
async function singleFetchLoaderNavigationStrategy(
@@ -374,11 +359,19 @@ async function singleFetchLoaderNavigationStrategy(
374359
return;
375360
}
376361

377-
// Otherwise, we opt out if we currently have data, a `loader`, and a
362+
// Otherwise, we opt out if we currently have data and a
378363
// `shouldRevalidate` function. This implies that the user opted out
379364
// via `shouldRevalidate`
380-
if (isOptedOut(manifestRoute, routeModules[m.route.id], m, router)) {
381-
foundOptOutRoute = true;
365+
if (
366+
m.route.id in router.state.loaderData &&
367+
manifestRoute &&
368+
m.route.shouldRevalidate
369+
) {
370+
if (manifestRoute.hasLoader) {
371+
// If we have a server loader, make sure we don't include it in the
372+
// single fetch .data request
373+
foundOptOutRoute = true;
374+
}
382375
return;
383376
}
384377
}

0 commit comments

Comments
 (0)