Skip to content

Commit

Permalink
Align single fetch prefetching with new revalidation logic (#9958)
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 authored Sep 6, 2024
1 parent b66a4b9 commit f4b4420
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 98 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-olives-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/react": patch
---

[REMOVE] Align single fetch prefetchign with new revalidation logic
212 changes: 208 additions & 4 deletions integration/single-fetch-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3253,9 +3253,9 @@ test.describe("single-fetch", () => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/", true);

// A/B can be prefetched, C doesn't get prefetched due to its `clientLoader`
// root/A/B can be prefetched, C doesn't get prefetched due to its `clientLoader`
await page.waitForSelector(
"nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=routes%2Fa%2Croutes%2Fa.b']",
"nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa%2Croutes%2Fa.b']",
{ state: "attached" }
);
expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(1);
Expand Down Expand Up @@ -3351,9 +3351,9 @@ test.describe("single-fetch", () => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/", true);

// Only A can get prefetched, B/C can't due to `clientLoader`
// root/A can get prefetched, B/C can't due to `clientLoader`
await page.waitForSelector(
"nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=routes%2Fa']",
"nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa']",
{ state: "attached" }
);
expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(1);
Expand All @@ -3368,6 +3368,35 @@ test.describe("single-fetch", () => {
},
files: {
...files,
"app/root.tsx": js`
import { Links, Meta, Outlet, Scripts } from "@remix-run/react";
export function loader() {
return {
message: "ROOT",
};
}
export async function clientLoader({ serverLoader }) {
let data = await serverLoader();
return { message: data.message + " (root client loader)" };
}
export default function Root() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}
`,
"app/routes/_index.tsx": js`
import { Link } from "@remix-run/react";
Expand Down Expand Up @@ -3457,6 +3486,181 @@ test.describe("single-fetch", () => {
// No prefetching due to clientLoaders
expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(0);
});

test("when a reused route opts out of revalidation", async ({ page }) => {
let fixture = await createFixture({
config: {
future: {
unstable_singleFetch: true,
},
},
files: {
...files,
"app/routes/a.tsx": js`
import { Link, Outlet, useLoaderData } from '@remix-run/react';
export function loader() {
return { message: "A server loader" };
}
export function shouldRevalidate() {
return false;
}
export default function Comp() {
let data = useLoaderData();
return (
<>
<h1>A</h1>
<p id="a-data">{data.message}</p>
<nav>
<Link to="/a/b/c" prefetch="render">/a/b/c</Link>
</nav>
<Outlet/>
</>
);
}
`,
"app/routes/a.b.tsx": js`
import { Outlet, useLoaderData } from '@remix-run/react';
export function loader() {
return { message: "B server loader" };
}
export default function Comp() {
let data = useLoaderData();
return (
<>
<h1>B</h1>
<p id="b-data">{data.message}</p>
<Outlet/>
</>
);
}
`,
"app/routes/a.b.c.tsx": js`
import { useLoaderData } from '@remix-run/react';
export function loader() {
return { message: "C server loader" };
}
export default function Comp() {
let data = useLoaderData();
return (
<>
<h1>C</h1>
<p id="c-data">{data.message}</p>
</>
);
}
`,
},
});

let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/a", true);

// A opted out of revalidation
await page.waitForSelector(
"link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa.b%2Croutes%2Fa.b.c']",
{ state: "attached" }
);
expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(1);
});

test("when a reused route opts out of revalidation and another route has a clientLoader", async ({
page,
}) => {
let fixture = await createFixture({
config: {
future: {
unstable_singleFetch: true,
},
},
files: {
...files,
"app/routes/a.tsx": js`
import { Link, Outlet, useLoaderData } from '@remix-run/react';
export function loader() {
return { message: "A server loader" };
}
export function shouldRevalidate() {
return false;
}
export default function Comp() {
let data = useLoaderData();
return (
<>
<h1>A</h1>
<p id="a-data">{data.message}</p>
<nav>
<Link to="/a/b/c" prefetch="render">/a/b/c</Link>
</nav>
<Outlet/>
</>
);
}
`,
"app/routes/a.b.tsx": js`
import { Outlet, useLoaderData } from '@remix-run/react';
export function loader() {
return { message: "B server loader" };
}
export default function Comp() {
let data = useLoaderData();
return (
<>
<h1>B</h1>
<p id="b-data">{data.message}</p>
<Outlet/>
</>
);
}
`,
"app/routes/a.b.c.tsx": js`
import { useLoaderData } from '@remix-run/react';
export function loader() {
return { message: "C server loader" };
}
export async function clientLoader({ serverLoader }) {
let data = await serverLoader();
return { message: data.message + " (C client loader)" };
}
export default function Comp() {
let data = useLoaderData();
return (
<>
<h1>C</h1>
<p id="c-data">{data.message}</p>
</>
);
}
`,
},
});

let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/a", true);

// A opted out of revalidation
await page.waitForSelector(
"nav link[rel='prefetch'][as='fetch'][href='/a/b/c.data?_routes=root%2Croutes%2Fa.b']",
{ state: "attached" }
);
expect(await app.page.locator("nav link[as='fetch']").count()).toEqual(1);
});
});

test("supports nonce on streaming script tags", async ({ page }) => {
Expand Down
108 changes: 68 additions & 40 deletions packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import type {
MetaMatches,
RouteHandle,
} from "./routeModules";
import { addRevalidationParam, singleFetchUrl } from "./single-fetch";
import { singleFetchUrl } from "./single-fetch";
import { getPartialManifest, isFogOfWarEnabled } from "./fog-of-war";

function useDataRouterContext() {
Expand Down Expand Up @@ -449,7 +449,7 @@ function PrefetchPageLinksImpl({
}) {
let location = useLocation();
let { future, manifest, routeModules } = useRemixContext();
let { matches } = useDataRouterStateContext();
let { loaderData, matches } = useDataRouterStateContext();

let newMatchesForData = React.useMemo(
() =>
Expand All @@ -464,6 +464,69 @@ function PrefetchPageLinksImpl({
[page, nextMatches, matches, manifest, location]
);

let dataHrefs = React.useMemo(() => {
if (!future.unstable_singleFetch) {
return getDataLinkHrefs(page, newMatchesForData, manifest);
}

if (page === location.pathname + location.search + location.hash) {
// Because we opt-into revalidation, don't compute this for the current page
// since it would always trigger a prefetch of the existing loaders
return [];
}

// Single-fetch is harder :)
// This parallels the logic in the single fetch data strategy
let routesParams = new Set<string>();
let foundOptOutRoute = false;
nextMatches.forEach((m) => {
if (!manifest.routes[m.route.id].hasLoader) {
return;
}

if (
!newMatchesForData.some((m2) => m2.route.id === m.route.id) &&
m.route.id in loaderData &&
routeModules[m.route.id]?.shouldRevalidate
) {
foundOptOutRoute = true;
} else if (manifest.routes[m.route.id].hasClientLoader) {
foundOptOutRoute = true;
} else {
routesParams.add(m.route.id);
}
});

if (routesParams.size === 0) {
return [];
}

let url = singleFetchUrl(page);
// When one or more routes have opted out, we add a _routes param to
// limit the loaders to those that have a server loader and did not
// opt out
if (foundOptOutRoute && routesParams.size > 0) {
url.searchParams.set(
"_routes",
nextMatches
.filter((m) => routesParams.has(m.route.id))
.map((m) => m.route.id)
.join(",")
);
}

return [url.pathname + url.search];
}, [
future.unstable_singleFetch,
loaderData,
location,
manifest,
newMatchesForData,
nextMatches,
page,
routeModules,
]);

let newMatchesForAssets = React.useMemo(
() =>
getNewMatchesForLinks(
Expand All @@ -477,11 +540,6 @@ function PrefetchPageLinksImpl({
[page, nextMatches, matches, manifest, location]
);

let dataHrefs = React.useMemo(
() => getDataLinkHrefs(page, newMatchesForData, manifest),
[newMatchesForData, page, manifest]
);

let moduleHrefs = React.useMemo(
() => getModuleLinkHrefs(newMatchesForAssets, manifest),
[newMatchesForAssets, manifest]
Expand All @@ -491,41 +549,11 @@ function PrefetchPageLinksImpl({
// just the manifest like the other links in here.
let keyedPrefetchLinks = useKeyedPrefetchLinks(newMatchesForAssets);

let linksToRender: React.ReactNode | React.ReactNode[] | null = null;
if (!future.unstable_singleFetch) {
// Non-single-fetch prefetching
linksToRender = dataHrefs.map((href) => (
<link key={href} rel="prefetch" as="fetch" href={href} {...linkProps} />
));
} else if (newMatchesForData.length > 0) {
// Single-fetch with routes that require data
let url = addRevalidationParam(
manifest,
routeModules,
nextMatches.map((m) => m.route),
newMatchesForData.map((m) => m.route),
singleFetchUrl(page)
);
if (url.searchParams.get("_routes") !== "") {
linksToRender = (
<link
key={url.pathname + url.search}
rel="prefetch"
as="fetch"
href={url.pathname + url.search}
{...linkProps}
/>
);
} else {
// No single-fetch prefetching if _routes param is empty due to `clientLoader`'s
}
} else {
// No single-fetch prefetching if there are no new matches for data
}

return (
<>
{linksToRender}
{dataHrefs.map((href) => (
<link key={href} rel="prefetch" as="fetch" href={href} {...linkProps} />
))}
{moduleHrefs.map((href) => (
<link key={href} rel="modulepreload" href={href} {...linkProps} />
))}
Expand Down
Loading

0 comments on commit f4b4420

Please sign in to comment.