-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add prefetch="viewport"
support to <Link>
#6433
Changes from all commits
e24f65d
331e8b3
4520f0f
acd4645
ee74d1b
fab2cce
44f28f3
c3e9ded
219057f
748d6a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"remix": minor | ||
"@remix-run/react": minor | ||
--- | ||
|
||
Add support for `<Link prefetch="viewport">` to prefetch links when they enter the viewport via an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -201,7 +201,7 @@ export function RemixRouteError({ id }: { id: string }) { | |
* - "render": Fetched when the link is rendered | ||
* - "none": Never fetched | ||
*/ | ||
type PrefetchBehavior = "intent" | "render" | "none"; | ||
type PrefetchBehavior = "intent" | "render" | "none" | "viewport"; | ||
|
||
export interface RemixLinkProps extends LinkProps { | ||
prefetch?: PrefetchBehavior; | ||
|
@@ -219,19 +219,35 @@ interface PrefetchHandlers { | |
onTouchStart?: TouchEventHandler; | ||
} | ||
|
||
function usePrefetchBehavior( | ||
function usePrefetchBehavior<T extends HTMLAnchorElement>( | ||
prefetch: PrefetchBehavior, | ||
theirElementProps: PrefetchHandlers | ||
): [boolean, Required<PrefetchHandlers>] { | ||
): [boolean, React.RefObject<T>, Required<PrefetchHandlers>] { | ||
let [maybePrefetch, setMaybePrefetch] = React.useState(false); | ||
let [shouldPrefetch, setShouldPrefetch] = React.useState(false); | ||
let { onFocus, onBlur, onMouseEnter, onMouseLeave, onTouchStart } = | ||
theirElementProps; | ||
|
||
let ref = React.useRef<T>(null); | ||
|
||
React.useEffect(() => { | ||
if (prefetch === "render") { | ||
setShouldPrefetch(true); | ||
} | ||
|
||
if (prefetch === "viewport") { | ||
let callback: IntersectionObserverCallback = (entries) => { | ||
entries.forEach((entry) => { | ||
setShouldPrefetch(entry.isIntersecting); | ||
}); | ||
}; | ||
let observer = new IntersectionObserver(callback, { threshold: 0.5 }); | ||
if (ref.current) observer.observe(ref.current); | ||
|
||
return () => { | ||
observer.disconnect(); | ||
}; | ||
} | ||
}, [prefetch]); | ||
|
||
let setIntent = () => { | ||
|
@@ -260,6 +276,7 @@ function usePrefetchBehavior( | |
|
||
return [ | ||
shouldPrefetch, | ||
ref, | ||
{ | ||
onFocus: composeEventHandlers(onFocus, setIntent), | ||
onBlur: composeEventHandlers(onBlur, cancelIntent), | ||
|
@@ -282,17 +299,18 @@ let NavLink = React.forwardRef<HTMLAnchorElement, RemixNavLinkProps>( | |
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); | ||
|
||
let href = useHref(to); | ||
let [shouldPrefetch, prefetchHandlers] = usePrefetchBehavior( | ||
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior( | ||
prefetch, | ||
props | ||
); | ||
|
||
return ( | ||
<> | ||
<RouterNavLink | ||
ref={forwardedRef} | ||
to={to} | ||
{...props} | ||
{...prefetchHandlers} | ||
ref={mergeRefs(forwardedRef, ref)} | ||
to={to} | ||
/> | ||
{shouldPrefetch && !isAbsolute ? ( | ||
<PrefetchPageLinks page={href} /> | ||
|
@@ -315,18 +333,18 @@ let Link = React.forwardRef<HTMLAnchorElement, RemixLinkProps>( | |
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); | ||
|
||
let href = useHref(to); | ||
let [shouldPrefetch, prefetchHandlers] = usePrefetchBehavior( | ||
let [shouldPrefetch, ref, prefetchHandlers] = usePrefetchBehavior( | ||
prefetch, | ||
props | ||
); | ||
|
||
return ( | ||
<> | ||
<RouterLink | ||
ref={forwardedRef} | ||
to={to} | ||
{...props} | ||
{...prefetchHandlers} | ||
ref={mergeRefs(forwardedRef, ref)} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any internal react overhead in attaching a ref? This feels potentially troublesome since we now attach a I took a stab at an opt-in ref approach in 44f28f3 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no perf hit per @jacob-ebey so going to back out that commit |
||
to={to} | ||
/> | ||
{shouldPrefetch && !isAbsolute ? ( | ||
<PrefetchPageLinks page={href} /> | ||
|
@@ -1805,3 +1823,17 @@ export const LiveReload = | |
/> | ||
); | ||
}; | ||
|
||
function mergeRefs<T = any>( | ||
...refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>> | ||
): React.RefCallback<T> { | ||
return (value) => { | ||
refs.forEach((ref) => { | ||
if (typeof ref === "function") { | ||
ref(value); | ||
} else if (ref != null) { | ||
(ref as React.MutableRefObject<T | null>).current = value; | ||
} | ||
}); | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the observer be disconnected after prefetch is set? That way the IntersectionObserver doesn't need to keep observing if
ref.current
is in the viewportThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mostly depends on if we should clean up link preloads when the link leaves the viewport. another thing would be that if it re-enters the vp, should it refetch the loader?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing/re-adding link tags if an element scrolled out of and back into the viewport would be consistent with our
prefetch="intent"
behavior