Leverage useLinkStatus
This commit is contained in:
parent
21f8392cc6
commit
df1f16f930
@ -1,94 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ComponentProps,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ComponentProps, ReactNode, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import LinkWithStatusChild from './primitives/LinkWithStatusChild';
|
||||
import clsx from 'clsx/lite';
|
||||
|
||||
// Avoid showing spinner for too short a time
|
||||
const FLICKER_THRESHOLD = 400;
|
||||
// Clear loading status after long duration
|
||||
const MAX_LOADING_DURATION = 15_000;
|
||||
|
||||
export default function LinkWithStatus({
|
||||
loadingClassName,
|
||||
href,
|
||||
className,
|
||||
onClick,
|
||||
children,
|
||||
className,
|
||||
loadingClassName,
|
||||
isLoading: isLoadingProp = false,
|
||||
setIsLoading: setIsLoadingProp,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof Link>, 'children'> & {
|
||||
}: ComponentProps<typeof Link> & {
|
||||
children: ReactNode | ((props: { isLoading: boolean }) => ReactNode)
|
||||
loadingClassName?: string
|
||||
// For hoisting state to a parent component, e.g., <EntityLink />
|
||||
isLoading?: boolean
|
||||
setIsLoading?: (isLoading: boolean) => void
|
||||
}) {
|
||||
const path = usePathname();
|
||||
|
||||
const [pathWhenClicked, setPathWhenClicked] = useState<string>();
|
||||
const [_isLoading, _setIsLoading] = useState(false);
|
||||
const isLoading = isLoadingProp || _isLoading;
|
||||
const setIsLoading = setIsLoadingProp || _setIsLoading;
|
||||
|
||||
const isLoadingStartTime = useRef<number | undefined>(undefined);
|
||||
|
||||
const startLoadingTimeout = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
const stopLoadingTimeout = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
const maxLoadingTimeout = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
|
||||
const isControlled = typeof children === 'function';
|
||||
|
||||
const clearTimeouts = useCallback(() => {
|
||||
[startLoadingTimeout, stopLoadingTimeout, maxLoadingTimeout]
|
||||
.forEach(timeout => {
|
||||
if (timeout.current) { clearTimeout(timeout.current); }
|
||||
});
|
||||
}, []);
|
||||
|
||||
const stopLoading = useCallback(() => {
|
||||
setIsLoading(false);
|
||||
setPathWhenClicked(undefined);
|
||||
}, [setIsLoading]);
|
||||
|
||||
const isVisitingLinkHref = path === href;
|
||||
|
||||
const shouldCancelLoading =
|
||||
(pathWhenClicked && pathWhenClicked !== path) ||
|
||||
isVisitingLinkHref;
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldCancelLoading) {
|
||||
clearTimeouts();
|
||||
const loadingDuration = isLoadingStartTime.current
|
||||
? Date.now() - isLoadingStartTime.current
|
||||
: 0;
|
||||
if (loadingDuration < FLICKER_THRESHOLD) {
|
||||
stopLoadingTimeout.current = setTimeout(
|
||||
stopLoading,
|
||||
FLICKER_THRESHOLD - loadingDuration,
|
||||
);
|
||||
} else {
|
||||
stopLoading();
|
||||
}
|
||||
}
|
||||
}, [shouldCancelLoading, clearTimeouts, stopLoading]);
|
||||
|
||||
// Clear timeouts when unmounting
|
||||
useEffect(() => () => clearTimeouts(), [clearTimeouts]);
|
||||
|
||||
return <Link
|
||||
{...props}
|
||||
href={href}
|
||||
className={clsx(
|
||||
'transition-[colors,opacity]',
|
||||
(loadingClassName || isControlled)
|
||||
@ -97,27 +35,11 @@ export default function LinkWithStatus({
|
||||
className,
|
||||
isLoading && loadingClassName,
|
||||
)}
|
||||
onClick={e => {
|
||||
const isOpeningNewTab = e.metaKey || e.ctrlKey;
|
||||
if (!isVisitingLinkHref && !isOpeningNewTab) {
|
||||
setPathWhenClicked(path);
|
||||
startLoadingTimeout.current = setTimeout(
|
||||
() => {
|
||||
isLoadingStartTime.current = Date.now();
|
||||
setIsLoading(true);
|
||||
},
|
||||
FLICKER_THRESHOLD,
|
||||
);
|
||||
maxLoadingTimeout.current = setTimeout(
|
||||
stopLoading,
|
||||
MAX_LOADING_DURATION,
|
||||
);
|
||||
}
|
||||
onClick?.(e);
|
||||
}}
|
||||
>
|
||||
{typeof children === 'function'
|
||||
? children({ isLoading })
|
||||
: children}
|
||||
<LinkWithStatusChild {...{ isLoading, setIsLoading }}>
|
||||
{typeof children === 'function'
|
||||
? children({ isLoading })
|
||||
: children}
|
||||
</LinkWithStatusChild>
|
||||
</Link>;
|
||||
}
|
||||
|
||||
52
src/components/primitives/LinkWithStatusChild.tsx
Normal file
52
src/components/primitives/LinkWithStatusChild.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode, useEffect, useRef } from 'react';
|
||||
import { useLinkStatus } from 'next/link';
|
||||
|
||||
const FLICKER_THRESHOLD = 400;
|
||||
|
||||
export default function LinkWithStatusChild({
|
||||
children,
|
||||
isLoading,
|
||||
setIsLoading,
|
||||
}: {
|
||||
children: ReactNode
|
||||
isLoading: boolean
|
||||
setIsLoading: (isLoading: boolean) => void
|
||||
}) {
|
||||
const { pending } = useLinkStatus();
|
||||
|
||||
const startLoadingTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
const stopLoadingTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
|
||||
const isLoadingStartTime = useRef<number>(undefined);
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
isLoadingStartTime.current = Date.now();
|
||||
} else {
|
||||
isLoadingStartTime.current = undefined;
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pending) {
|
||||
clearTimeout(stopLoadingTimeout.current);
|
||||
startLoadingTimeout.current = setTimeout(() => {
|
||||
setIsLoading(true);
|
||||
}, FLICKER_THRESHOLD);
|
||||
} else if (isLoadingStartTime.current) {
|
||||
clearTimeout(startLoadingTimeout.current);
|
||||
const loadingDuration = Date.now() - isLoadingStartTime.current;
|
||||
stopLoadingTimeout.current = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, FLICKER_THRESHOLD - loadingDuration);
|
||||
}
|
||||
}, [pending, setIsLoading]);
|
||||
|
||||
useEffect(() => () => {
|
||||
clearTimeout(startLoadingTimeout.current);
|
||||
clearTimeout(stopLoadingTimeout.current);
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user