Skip to content

Commit

Permalink
feat: support preload=viewport
Browse files Browse the repository at this point in the history
  • Loading branch information
sorrycc committed Dec 12, 2024
1 parent 29323a9 commit 46f2d38
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 57 deletions.
145 changes: 88 additions & 57 deletions packages/renderer-react/src/link.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,98 @@
import React, { PropsWithChildren, useLayoutEffect } from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { useAppData } from './appContext';
import { useIntersectionObserver } from './useIntersectionObserver';

export function LinkWithPrefetch(
props: PropsWithChildren<
{
prefetch?: boolean | 'intent' | 'render' | 'viewport' | 'none';
} & LinkProps &
React.RefAttributes<HTMLAnchorElement>
>,
) {
const { prefetch: prefetchProp, ...linkProps } = props;
const prefetch =
prefetchProp === true
? 'intent'
: prefetchProp === false
? 'none'
: prefetchProp;
const appData = useAppData();
const to = typeof props.to === 'string' ? props.to : props.to?.pathname;
const hasRenderFetched = React.useRef(false);
function useForwardedRef<T>(ref?: React.ForwardedRef<T>) {
const innerRef = React.useRef<T>(null);
React.useEffect(() => {
if (!ref) return;
if (typeof ref === 'function') {
ref(innerRef.current);
} else {
ref.current = innerRef.current;
}
});
return innerRef;
}

const handleMouseEnter = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (prefetch !== 'intent') return;
const eventTarget = (e.target || {}) as HTMLElement & {
preloadTimeout?: NodeJS.Timeout | null;
export const LinkWithPrefetch = React.forwardRef(
(
props: PropsWithChildren<
{
prefetch?: boolean | 'intent' | 'render' | 'viewport' | 'none';
} & LinkProps &
React.RefAttributes<HTMLAnchorElement>
>,
forwardedRef,
) => {
const { prefetch: prefetchProp, ...linkProps } = props;
const prefetch =
prefetchProp === true
? 'intent'
: prefetchProp === false
? 'none'
: prefetchProp;
const appData = useAppData();
const to = typeof props.to === 'string' ? props.to : props.to?.pathname;
const hasRenderFetched = React.useRef(false);
const ref = useForwardedRef(forwardedRef);
// prefetch intent
const handleMouseEnter = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (prefetch !== 'intent') return;
const eventTarget = (e.target || {}) as HTMLElement & {
preloadTimeout?: NodeJS.Timeout | null;
};
if (eventTarget.preloadTimeout) return;
eventTarget.preloadTimeout = setTimeout(() => {
eventTarget.preloadTimeout = null;
appData.preloadRoute?.(to!);
}, 50);
};
if (eventTarget.preloadTimeout) return;
eventTarget.preloadTimeout = setTimeout(() => {
eventTarget.preloadTimeout = null;
appData.preloadRoute?.(to!);
}, 50);
};

const handleMouseLeave = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (prefetch !== 'intent') return;
const eventTarget = (e.target || {}) as HTMLElement & {
preloadTimeout?: NodeJS.Timeout | null;
const handleMouseLeave = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (prefetch !== 'intent') return;
const eventTarget = (e.target || {}) as HTMLElement & {
preloadTimeout?: NodeJS.Timeout | null;
};
if (eventTarget.preloadTimeout) {
clearTimeout(eventTarget.preloadTimeout);
eventTarget.preloadTimeout = null;
}
};
if (eventTarget.preloadTimeout) {
clearTimeout(eventTarget.preloadTimeout);
eventTarget.preloadTimeout = null;
}
};

useLayoutEffect(() => {
if (prefetch === 'render' && !hasRenderFetched.current) {
appData.preloadRoute?.(to!);
hasRenderFetched.current = true;
}
}, [prefetch, to]);
// prefetch render
useLayoutEffect(() => {
if (prefetch === 'render' && !hasRenderFetched.current) {
appData.preloadRoute?.(to!);
hasRenderFetched.current = true;
}
}, [prefetch, to]);

// compatible with old code
// which to might be undefined
if (!to) return null;
// prefetch viewport
useIntersectionObserver(
ref as React.RefObject<HTMLAnchorElement>,
(entry) => {
if (entry?.isIntersecting) {
appData.preloadRoute?.(to!);
}
},
{ rootMargin: '100px' },
{ disabled: prefetch !== 'viewport' },
);

return (
<Link
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
{...linkProps}
>
{props.children}
</Link>
);
}
// compatible with old code
// which to might be undefined
if (!to) return null;

return (
<Link
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={ref as React.RefObject<HTMLAnchorElement>}
{...linkProps}
>
{props.children}
</Link>
);
},
);
33 changes: 33 additions & 0 deletions packages/renderer-react/src/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';

export function useIntersectionObserver<T extends Element>(
ref: React.RefObject<T>,
callback: (entry: IntersectionObserverEntry | undefined) => void,
intersectionObserverOptions: IntersectionObserverInit = {},
options: { disabled?: boolean } = {},
): IntersectionObserver | null {
// check if IntersectionObserver is available
if (typeof IntersectionObserver !== 'function') return null;

const isIntersectionObserverAvailable = React.useRef(
typeof IntersectionObserver === 'function',
);
const observerRef = React.useRef<IntersectionObserver | null>(null);
React.useEffect(() => {
if (
!ref.current ||
!isIntersectionObserverAvailable.current ||
options.disabled
) {
return;
}
observerRef.current = new IntersectionObserver(([entry]) => {
callback(entry);
}, intersectionObserverOptions);
observerRef.current.observe(ref.current);
return () => {
observerRef.current?.disconnect();
};
}, [callback, intersectionObserverOptions, options.disabled, ref]);
return observerRef.current;
}

0 comments on commit 46f2d38

Please sign in to comment.