'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 of inactivity const MAX_LOADING_DURATION = 15_000; export type LinkWithStatusProps = ComponentProps & { loadingElement?: ReactNode loadingClassName?: string contentClassName?: string } export default function LinkWithStatus({ loadingElement, loadingClassName, contentClassName, href, className, onClick, children, ...props }: LinkWithStatusProps) { const path = usePathname(); const [isLoading, setIsLoading] = useState(false); const isLoadingStartTime = useRef(undefined); const startLoadingTimeout = useRef(undefined); const stopLoadingTimeout = useRef(undefined); const maxLoadingTimeout = useRef(undefined); const clearTimeouts = useCallback(() => [startLoadingTimeout, stopLoadingTimeout, maxLoadingTimeout] .forEach(timeout => { if (timeout.current) { clearTimeout(timeout.current); } }), []); const isVisitingLinkHref = path === href; useEffect(() => { if (isVisitingLinkHref) { clearTimeouts(); const loadingDuration = isLoadingStartTime.current ? Date.now() - isLoadingStartTime.current : 0; if (loadingDuration < FLICKER_THRESHOLD) { stopLoadingTimeout.current = setTimeout( () => { setIsLoading(false); }, FLICKER_THRESHOLD - loadingDuration, ); } else { setIsLoading(false); } } }, [isVisitingLinkHref, clearTimeouts]); // Clear timeouts when unmounting useEffect(() => () => clearTimeouts(), [clearTimeouts]); return { const isOpeningNewTab = e.metaKey || e.ctrlKey; if (!isVisitingLinkHref && !isOpeningNewTab) { startLoadingTimeout.current = setTimeout( () => { isLoadingStartTime.current = Date.now(); setIsLoading(true); }, FLICKER_THRESHOLD, ); maxLoadingTimeout.current = setTimeout( () => { setIsLoading(false); }, MAX_LOADING_DURATION, ); } onClick?.(e); }} > {children} {isLoading && loadingElement && {loadingElement} } ; }