diff --git a/.changeset/link-prefetching.md b/.changeset/link-prefetching.md new file mode 100644 index 0000000000..0482a5c1b2 --- /dev/null +++ b/.changeset/link-prefetching.md @@ -0,0 +1,5 @@ +--- +"react-router-dom": minor +--- + +Add prefetching support to `Link`/`NavLink` when using Remix SSR diff --git a/packages/react-router-dom/__tests__/ssr/components-test.tsx b/packages/react-router-dom/__tests__/ssr/components-test.tsx index 30645d4b9c..2ab93d1f2f 100644 --- a/packages/react-router-dom/__tests__/ssr/components-test.tsx +++ b/packages/react-router-dom/__tests__/ssr/components-test.tsx @@ -4,12 +4,14 @@ import * as React from "react"; import { createMemoryRouter, + Link, + NavLink, Outlet, RouterProvider, _setSsrInfoForTests, } from "../../index"; import type { LiveReload as ActualLiveReload } from "../../ssr/components"; -import { Link, NavLink, RemixContext } from "../../ssr/components"; +import { RemixContext } from "../../ssr/components"; import invariant from "../../ssr/invariant"; import { RemixServer } from "../../ssr/server"; import "@testing-library/jest-dom/extend-expect"; diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index 93bc662a9a..28e2f1e3b2 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -82,8 +82,13 @@ import { shouldProcessLinkClick, } from "./dom"; -import type { ScriptProps, UIMatch } from "./ssr/components"; -import { RemixContext } from "./ssr/components"; +import type { PrefetchBehavior, ScriptProps, UIMatch } from "./ssr/components"; +import { + PrefetchPageLinks, + RemixContext, + mergeRefs, + usePrefetchBehavior, +} from "./ssr/components"; import type { AssetsManifest, FutureConfig as RemixFutureConfig, @@ -1401,6 +1406,7 @@ export { HistoryRouter as unstable_HistoryRouter }; export interface LinkProps extends Omit, "href"> { + prefetch?: PrefetchBehavior; reloadDocument?: boolean; replace?: boolean; state?: any; @@ -1424,6 +1430,7 @@ export const Link = React.forwardRef( function LinkWithRef( { onClick, + prefetch = "none", relative, reloadDocument, replace, @@ -1434,15 +1441,16 @@ export const Link = React.forwardRef( unstable_viewTransition, ...rest }, - ref + forwardedRef ) { let { basename } = React.useContext(NavigationContext); + let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); // Rendered into for absolute URLs let absoluteHref; let isExternal = false; - if (typeof to === "string" && ABSOLUTE_URL_REGEX.test(to)) { + if (typeof to === "string" && isAbsolute) { // Render the absolute href server- and client-side absoluteHref = to; @@ -1474,6 +1482,10 @@ export const Link = React.forwardRef( // Rendered into for relative URLs let href = useHref(to, { relative }); + let [shouldPrefetch, prefetchRef, prefetchHandlers] = usePrefetchBehavior( + prefetch, + rest + ); let internalOnClick = useLinkClickHandler(to, { replace, @@ -1492,16 +1504,26 @@ export const Link = React.forwardRef( } } - return ( + let link = ( // eslint-disable-next-line jsx-a11y/anchor-has-content ); + + return shouldPrefetch && !isAbsolute ? ( + <> + {link} + + + ) : ( + link + ); } ); diff --git a/packages/react-router-dom/ssr/components.tsx b/packages/react-router-dom/ssr/components.tsx index 3ff9ab938b..0b00cf1bae 100644 --- a/packages/react-router-dom/ssr/components.tsx +++ b/packages/react-router-dom/ssr/components.tsx @@ -23,15 +23,10 @@ import { useRouteLoaderData as useRouteLoaderDataRR, useLocation, useNavigation, - useHref, } from "react-router"; -import type { FetcherWithComponents, LinkProps, NavLinkProps } from "../index"; -import { - Link as RouterLink, - NavLink as RouterNavLink, - useFetcher as useFetcherRR, -} from "../index"; +import type { FetcherWithComponents } from "../index"; +import { useFetcher as useFetcherRR } from "../index"; import type { AppData } from "./data"; import type { RemixContextObject } from "./entry"; import invariant from "./invariant"; @@ -100,15 +95,7 @@ export function useRemixContext(): RemixContextObject { * - "render": Fetched when the link is rendered * - "viewport": Fetched when the link is in the viewport */ -type PrefetchBehavior = "intent" | "render" | "none" | "viewport"; - -export interface RemixLinkProps extends LinkProps { - prefetch?: PrefetchBehavior; -} - -export interface RemixNavLinkProps extends NavLinkProps { - prefetch?: PrefetchBehavior; -} +export type PrefetchBehavior = "intent" | "render" | "none" | "viewport"; interface PrefetchHandlers { onFocus?: FocusEventHandler; @@ -118,10 +105,11 @@ interface PrefetchHandlers { onTouchStart?: TouchEventHandler; } -function usePrefetchBehavior( +export function usePrefetchBehavior( prefetch: PrefetchBehavior, theirElementProps: PrefetchHandlers -): [boolean, React.RefObject, Required] { +): [boolean, React.RefObject, PrefetchHandlers] { + let remixContext = React.useContext(RemixContext); let [maybePrefetch, setMaybePrefetch] = React.useState(false); let [shouldPrefetch, setShouldPrefetch] = React.useState(false); let { onFocus, onBlur, onMouseEnter, onMouseLeave, onTouchStart } = @@ -149,19 +137,6 @@ function usePrefetchBehavior( } }, [prefetch]); - let setIntent = () => { - if (prefetch === "intent") { - setMaybePrefetch(true); - } - }; - - let cancelIntent = () => { - if (prefetch === "intent") { - setMaybePrefetch(false); - setShouldPrefetch(false); - } - }; - React.useEffect(() => { if (maybePrefetch) { let id = setTimeout(() => { @@ -173,6 +148,25 @@ function usePrefetchBehavior( } }, [maybePrefetch]); + let setIntent = () => { + setMaybePrefetch(true); + }; + + let cancelIntent = () => { + setMaybePrefetch(false); + setShouldPrefetch(false); + }; + + // No prefetching if not using Remix-style SSR + if (!remixContext) { + return [false, ref, {}]; + } + + if (prefetch !== "intent") { + return [shouldPrefetch, ref, {}]; + } + + // When using prefetch="intent" we need to attach focus/hover listeners return [ shouldPrefetch, ref, @@ -186,75 +180,6 @@ function usePrefetchBehavior( ]; } -const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; - -/** - * A special kind of `` that knows whether it is "active". - * - * @see https://remix.run/components/nav-link - */ -let NavLink = React.forwardRef( - ({ to, prefetch = "none", ...props }, forwardedRef) => { - let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); - - let href = useHref(to); - let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior( - prefetch, - props - ); - - return ( - <> - - {shouldPrefetch && !isAbsolute ? ( - - ) : null} - - ); - } -); -NavLink.displayName = "NavLink"; -export { NavLink }; - -/** - * This component renders an anchor tag and is the primary way the user will - * navigate around your website. - * - * @see https://remix.run/components/link - */ -let Link = React.forwardRef( - ({ to, prefetch = "none", ...props }, forwardedRef) => { - let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); - - let href = useHref(to); - let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior( - prefetch, - props - ); - - return ( - <> - - {shouldPrefetch && !isAbsolute ? ( - - ) : null} - - ); - } -); -Link.displayName = "Link"; -export { Link }; - export function composeEventHandlers< EventType extends React.SyntheticEvent | Event >( @@ -1257,7 +1182,7 @@ export const LiveReload = ); }; -function mergeRefs( +export function mergeRefs( ...refs: Array | React.LegacyRef> ): React.RefCallback { return (value) => {