'use client'; import { ComponentProps, ReactNode, useCallback, useEffect, useRef, useState, } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; 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 type LinkWithStatusProps = Omit< ComponentProps, 'children' > & { loadingElement?: ReactNode loadingClassName?: string contentClassName?: string children: ReactNode | ((props: { isLoading: boolean }) => ReactNode) } export default function LinkWithStatus({ loadingElement, loadingClassName, contentClassName, href, className, onClick, children, ...props }: LinkWithStatusProps) { const path = usePathname(); const [pathWhenClicked, setPathWhenClicked] = useState(); const [isLoading, setIsLoading] = useState(false); const isLoadingStartTime = useRef(undefined); const startLoadingTimeout = useRef(undefined); const stopLoadingTimeout = useRef(undefined); const maxLoadingTimeout = useRef(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); }, []); 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 { 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} {isLoading && loadingElement && {loadingElement} } ; }