Skip to content

Commit

Permalink
Add configurable wait to PrefetchableLink when mode is "intent"
Browse files Browse the repository at this point in the history
- resolve loaders in parallel
  • Loading branch information
solomonhawk committed Oct 29, 2024
1 parent 1e4f6e6 commit c9f4462
Showing 1 changed file with 72 additions and 22 deletions.
94 changes: 72 additions & 22 deletions apps/web/src/features/routing/components/prefetchable-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,19 @@ import { useCallback, useEffect, useRef } from "react";
import { Link, type LinkProps, matchRoutes } from "react-router-dom";

import { routesAtom } from "~features/routing/state";
import { log } from "~utils/logger";

export type PrefetchBehavior = "visible" | "intent";
type PrefetchableLinkProps = LinkProps &
(
| {
mode?: "visible";
wait?: never;
}
| {
mode?: "intent";
wait?: number;
}
);

/**
* @NOTE: Does not support following loader redirects and prefetching those
Expand All @@ -14,11 +25,15 @@ export function PrefetchableLink({
children,
to,
mode = "intent",
wait = 250,
...props
}: LinkProps & {
mode?: PrefetchBehavior;
}) {
}: PrefetchableLinkProps) {
if (import.meta.env.DEV && wait < 0) {
log.warn("PrefecthableLink: `wait` must be a positive number");
}

const ref = useRef<HTMLAnchorElement | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const routes = useAtomValue(routesAtom);

/**
Expand All @@ -32,31 +47,64 @@ export function PrefetchableLink({
return;
}

for (const match of nextMatches) {
const lazy = match.route.lazy;
const promises = [];

if (lazy) {
// @TODO: do we need to ignore certain exports here instead of spreading everything?
match.route = { ...match.route, ...(await lazy()) };
}

const loader = match.route.loader;

if (typeof loader === "function") {
// @PERF: would be nice to do these in parallel
await loader({
request: new Request(new URL(path, window.location.origin)),
params: match.params,
});
}
for (const match of nextMatches) {
/**
* If the route has a `lazy` definition, call it to load the lazy route
* definition. That definition may include a loader, but may not.
*
* If it doesn't, just use the matched route's existing loader, if present.
*/
const lazyLoadRoute =
match.route.lazy ??
(() => Promise.resolve({ loader: match.route.loader }));

promises.push(
lazyLoadRoute()
.then((module) => {
const loader = module.loader ?? match.route.loader;

if (typeof loader !== "function") {
return;
}

return loader({
request: new Request(new URL(path, window.location.origin)),
params: match.params,
});
})
.catch((e) => log.error(e)),
);
}

return Promise.all(promises);
}, [routes, to]);

const handleIntent = useCallback(() => {
if (mode === "intent") {
runLoaders();
if (wait > 0) {
const timeoutId = setTimeout(runLoaders, wait);
timeoutRef.current = timeoutId;
} else {
runLoaders();
}

return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}
}, [mode, runLoaders]);

return;
}, [mode, runLoaders, wait]);

const handleUnintent = useCallback(() => {
if (mode === "intent" && timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
}, [mode]);

useEffect(() => {
if (mode !== "visible") {
Expand Down Expand Up @@ -90,7 +138,9 @@ export function PrefetchableLink({
to={to}
{...props}
onMouseEnter={handleIntent}
onMouseLeave={handleUnintent}
onFocus={handleIntent}
onBlur={handleUnintent}
>
{children}
</Link>
Expand Down

0 comments on commit c9f4462

Please sign in to comment.