Skip to content

Commit

Permalink
Align single fetch prefetching with new revalidation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
brophdawg11 committed Sep 6, 2024
1 parent caea77d commit 4b9ba88
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 34 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
96 changes: 66 additions & 30 deletions packages/remix-react/components.tsx
Original file line number Diff line number Diff line change
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 Down Expand Up @@ -491,37 +491,73 @@ 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 {
let linksToRender = React.useMemo(() => {
if (!future.unstable_singleFetch) {
// Non-single-fetch prefetching is easy...
return dataHrefs.map((href) => (
<link key={href} rel="prefetch" as="fetch" href={href} {...linkProps} />
));
}

// 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) {
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) {
// No single-fetch prefetching if _routes param is empty due to `clientLoader`'s
return null;
}
} else {
// No single-fetch prefetching if there are no new matches for data
}

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 (
<link
key={url.pathname + url.search}
rel="prefetch"
as="fetch"
href={url.pathname + url.search}
{...linkProps}
/>
);
}, [
dataHrefs,
future.unstable_singleFetch,
linkProps,
loaderData,
manifest.routes,
newMatchesForData,
nextMatches,
page,
routeModules,
]);

return (
<>
Expand Down

0 comments on commit 4b9ba88

Please sign in to comment.