Skip to content

Commit d7dc327

Browse files
committed
Add prefetching support to Link/NavLink
1 parent 74be768 commit d7dc327

File tree

4 files changed

+62
-108
lines changed

4 files changed

+62
-108
lines changed

.changeset/link-prefetching.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": minor
3+
---
4+
5+
Add prefetching support to `Link`/`NavLink` when using Remix SSR

packages/react-router-dom/__tests__/ssr/components-test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import * as React from "react";
44

55
import {
66
createMemoryRouter,
7+
Link,
8+
NavLink,
79
Outlet,
810
RouterProvider,
911
_setSsrInfoForTests,
1012
} from "../../index";
1113
import type { LiveReload as ActualLiveReload } from "../../ssr/components";
12-
import { Link, NavLink, RemixContext } from "../../ssr/components";
14+
import { RemixContext } from "../../ssr/components";
1315
import invariant from "../../ssr/invariant";
1416
import { RemixServer } from "../../ssr/server";
1517
import "@testing-library/jest-dom/extend-expect";

packages/react-router-dom/index.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,13 @@ import {
8282
shouldProcessLinkClick,
8383
} from "./dom";
8484

85-
import type { ScriptProps, UIMatch } from "./ssr/components";
86-
import { RemixContext } from "./ssr/components";
85+
import type { PrefetchBehavior, ScriptProps, UIMatch } from "./ssr/components";
86+
import {
87+
PrefetchPageLinks,
88+
RemixContext,
89+
mergeRefs,
90+
usePrefetchBehavior,
91+
} from "./ssr/components";
8792
import type {
8893
AssetsManifest,
8994
FutureConfig as RemixFutureConfig,
@@ -1401,6 +1406,7 @@ export { HistoryRouter as unstable_HistoryRouter };
14011406

14021407
export interface LinkProps
14031408
extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
1409+
prefetch?: PrefetchBehavior;
14041410
reloadDocument?: boolean;
14051411
replace?: boolean;
14061412
state?: any;
@@ -1424,6 +1430,7 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
14241430
function LinkWithRef(
14251431
{
14261432
onClick,
1433+
prefetch = "none",
14271434
relative,
14281435
reloadDocument,
14291436
replace,
@@ -1434,15 +1441,16 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
14341441
unstable_viewTransition,
14351442
...rest
14361443
},
1437-
ref
1444+
forwardedRef
14381445
) {
14391446
let { basename } = React.useContext(NavigationContext);
1447+
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to);
14401448

14411449
// Rendered into <a href> for absolute URLs
14421450
let absoluteHref;
14431451
let isExternal = false;
14441452

1445-
if (typeof to === "string" && ABSOLUTE_URL_REGEX.test(to)) {
1453+
if (typeof to === "string" && isAbsolute) {
14461454
// Render the absolute href server- and client-side
14471455
absoluteHref = to;
14481456

@@ -1474,6 +1482,10 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
14741482

14751483
// Rendered into <a href> for relative URLs
14761484
let href = useHref(to, { relative });
1485+
let [shouldPrefetch, prefetchRef, prefetchHandlers] = usePrefetchBehavior(
1486+
prefetch,
1487+
rest
1488+
);
14771489

14781490
let internalOnClick = useLinkClickHandler(to, {
14791491
replace,
@@ -1492,16 +1504,26 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
14921504
}
14931505
}
14941506

1495-
return (
1507+
let link = (
14961508
// eslint-disable-next-line jsx-a11y/anchor-has-content
14971509
<a
14981510
{...rest}
1511+
{...prefetchHandlers}
14991512
href={absoluteHref || href}
15001513
onClick={isExternal || reloadDocument ? onClick : handleClick}
1501-
ref={ref}
1514+
ref={mergeRefs(forwardedRef, prefetchRef)}
15021515
target={target}
15031516
/>
15041517
);
1518+
1519+
return shouldPrefetch && !isAbsolute ? (
1520+
<>
1521+
{link}
1522+
<PrefetchPageLinks page={href} />
1523+
</>
1524+
) : (
1525+
link
1526+
);
15051527
}
15061528
);
15071529

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

Lines changed: 26 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,10 @@ import {
2323
useRouteLoaderData as useRouteLoaderDataRR,
2424
useLocation,
2525
useNavigation,
26-
useHref,
2726
} from "react-router";
2827

29-
import type { FetcherWithComponents, LinkProps, NavLinkProps } from "../index";
30-
import {
31-
Link as RouterLink,
32-
NavLink as RouterNavLink,
33-
useFetcher as useFetcherRR,
34-
} from "../index";
28+
import type { FetcherWithComponents } from "../index";
29+
import { useFetcher as useFetcherRR } from "../index";
3530
import type { AppData } from "./data";
3631
import type { RemixContextObject } from "./entry";
3732
import invariant from "./invariant";
@@ -100,15 +95,7 @@ export function useRemixContext(): RemixContextObject {
10095
* - "render": Fetched when the link is rendered
10196
* - "viewport": Fetched when the link is in the viewport
10297
*/
103-
type PrefetchBehavior = "intent" | "render" | "none" | "viewport";
104-
105-
export interface RemixLinkProps extends LinkProps {
106-
prefetch?: PrefetchBehavior;
107-
}
108-
109-
export interface RemixNavLinkProps extends NavLinkProps {
110-
prefetch?: PrefetchBehavior;
111-
}
98+
export type PrefetchBehavior = "intent" | "render" | "none" | "viewport";
11299

113100
interface PrefetchHandlers {
114101
onFocus?: FocusEventHandler;
@@ -118,10 +105,11 @@ interface PrefetchHandlers {
118105
onTouchStart?: TouchEventHandler;
119106
}
120107

121-
function usePrefetchBehavior<T extends HTMLAnchorElement>(
108+
export function usePrefetchBehavior<T extends HTMLAnchorElement>(
122109
prefetch: PrefetchBehavior,
123110
theirElementProps: PrefetchHandlers
124-
): [boolean, React.RefObject<T>, Required<PrefetchHandlers>] {
111+
): [boolean, React.RefObject<T>, PrefetchHandlers] {
112+
let remixContext = React.useContext(RemixContext);
125113
let [maybePrefetch, setMaybePrefetch] = React.useState(false);
126114
let [shouldPrefetch, setShouldPrefetch] = React.useState(false);
127115
let { onFocus, onBlur, onMouseEnter, onMouseLeave, onTouchStart } =
@@ -149,19 +137,6 @@ function usePrefetchBehavior<T extends HTMLAnchorElement>(
149137
}
150138
}, [prefetch]);
151139

152-
let setIntent = () => {
153-
if (prefetch === "intent") {
154-
setMaybePrefetch(true);
155-
}
156-
};
157-
158-
let cancelIntent = () => {
159-
if (prefetch === "intent") {
160-
setMaybePrefetch(false);
161-
setShouldPrefetch(false);
162-
}
163-
};
164-
165140
React.useEffect(() => {
166141
if (maybePrefetch) {
167142
let id = setTimeout(() => {
@@ -173,6 +148,25 @@ function usePrefetchBehavior<T extends HTMLAnchorElement>(
173148
}
174149
}, [maybePrefetch]);
175150

151+
let setIntent = () => {
152+
setMaybePrefetch(true);
153+
};
154+
155+
let cancelIntent = () => {
156+
setMaybePrefetch(false);
157+
setShouldPrefetch(false);
158+
};
159+
160+
// No prefetching if not using Remix-style SSR
161+
if (!remixContext) {
162+
return [false, ref, {}];
163+
}
164+
165+
if (prefetch !== "intent") {
166+
return [shouldPrefetch, ref, {}];
167+
}
168+
169+
// When using prefetch="intent" we need to attach focus/hover listeners
176170
return [
177171
shouldPrefetch,
178172
ref,
@@ -186,75 +180,6 @@ function usePrefetchBehavior<T extends HTMLAnchorElement>(
186180
];
187181
}
188182

189-
const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
190-
191-
/**
192-
* A special kind of `<Link>` that knows whether it is "active".
193-
*
194-
* @see https://remix.run/components/nav-link
195-
*/
196-
let NavLink = React.forwardRef<HTMLAnchorElement, RemixNavLinkProps>(
197-
({ to, prefetch = "none", ...props }, forwardedRef) => {
198-
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to);
199-
200-
let href = useHref(to);
201-
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior(
202-
prefetch,
203-
props
204-
);
205-
206-
return (
207-
<>
208-
<RouterNavLink
209-
{...props}
210-
{...prefetchHandlers}
211-
ref={mergeRefs(forwardedRef, ref)}
212-
to={to}
213-
/>
214-
{shouldPrefetch && !isAbsolute ? (
215-
<PrefetchPageLinks page={href} />
216-
) : null}
217-
</>
218-
);
219-
}
220-
);
221-
NavLink.displayName = "NavLink";
222-
export { NavLink };
223-
224-
/**
225-
* This component renders an anchor tag and is the primary way the user will
226-
* navigate around your website.
227-
*
228-
* @see https://remix.run/components/link
229-
*/
230-
let Link = React.forwardRef<HTMLAnchorElement, RemixLinkProps>(
231-
({ to, prefetch = "none", ...props }, forwardedRef) => {
232-
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to);
233-
234-
let href = useHref(to);
235-
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior(
236-
prefetch,
237-
props
238-
);
239-
240-
return (
241-
<>
242-
<RouterLink
243-
{...props}
244-
{...prefetchHandlers}
245-
ref={mergeRefs(forwardedRef, ref)}
246-
to={to}
247-
/>
248-
{shouldPrefetch && !isAbsolute ? (
249-
<PrefetchPageLinks page={href} />
250-
) : null}
251-
</>
252-
);
253-
}
254-
);
255-
Link.displayName = "Link";
256-
export { Link };
257-
258183
export function composeEventHandlers<
259184
EventType extends React.SyntheticEvent | Event
260185
>(
@@ -1257,7 +1182,7 @@ export const LiveReload =
12571182
);
12581183
};
12591184

1260-
function mergeRefs<T = any>(
1185+
export function mergeRefs<T = any>(
12611186
...refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>
12621187
): React.RefCallback<T> {
12631188
return (value) => {

0 commit comments

Comments
 (0)