'use client'; import { useCallback, useEffect, useRef } from 'react'; import Spinner from './Spinner'; import SiteGrid from './SiteGrid'; import { MoreComponentsKey, MoreComponentsStateForKeyArgument, useMoreComponentsState, } from '@/state/MoreComponentsState'; const MAX_ATTEMPTS_PER_REQUEST = 5; const MAX_TOTAL_REQUESTS = 100; const RETRY_DELAY_IN_SECONDS = 1; export default function MoreComponents({ stateKey, initialOffset, itemsPerRequest, getNextComponent, label = 'Load more', triggerOnView = true, prefetch = true, itemsClass, wrapMoreButtonInSiteGrid, debug, }: { stateKey: MoreComponentsKey initialOffset: number itemsPerRequest: number getNextComponent: (offset: number, limit: number) => Promise<{ nextComponent?: JSX.Element, isFinished?: boolean, didFail?: boolean, }> label?: string triggerOnView?: boolean prefetch?: boolean itemsClass?: string wrapMoreButtonInSiteGrid?: boolean debug?: boolean }) { const { state, setStateForKey } = useMoreComponentsState(); const setState = useCallback( (stateForKey: MoreComponentsStateForKeyArgument) => setStateForKey(stateKey, stateForKey), [setStateForKey, stateKey]); useEffect(() => { setState({ hasMounted: true }); }, [setState]); const { hasMounted, isLoading, indexInView, finalIndex, didReachMaximumRequests, components, } = state[stateKey]; // When prefetching, always stay one request ahead of what's visible const furthestIndexToLoad = Math.min( prefetch ? (indexInView ?? 0) + 1 : (indexInView ?? 0), finalIndex ?? Infinity, ); const indexToLoad = Math.min( components.length, furthestIndexToLoad, ); const attemptsPerRequest = useRef(0); const totalRequests = useRef(0); const hasFinalIndexBeenReached = finalIndex !== undefined && finalIndex <= components.length - 1; const areAllComponentsVisible = (indexInView ?? 0) >= components.length - 1; const showMoreButton = isLoading || !hasFinalIndexBeenReached || !areAllComponentsVisible; const currentTimeout = useRef(); const attempt = useCallback(() => { // Consider creating temp, anonymous function // for error handling const attemptRetry = () => { if (currentTimeout.current) { clearTimeout(currentTimeout.current); } currentTimeout.current = setTimeout(attempt, RETRY_DELAY_IN_SECONDS * 1000); }; if (attemptsPerRequest.current < MAX_ATTEMPTS_PER_REQUEST) { if (totalRequests.current < MAX_TOTAL_REQUESTS) { attemptsPerRequest.current += 1; totalRequests.current += 1; setState({ isLoading: true }); if (debug) { // eslint-disable-next-line max-len console.log(`GETTING INDEX: #${indexToLoad}, ATTEMPT: #${attemptsPerRequest.current}`); } getNextComponent( initialOffset + indexToLoad * itemsPerRequest, itemsPerRequest, ) .then(({ nextComponent, isFinished, didFail }) => { if (!didFail) { attemptsPerRequest.current = 0; setState(state => { const updatedComponents = [...state.components]; if (nextComponent) { updatedComponents[indexToLoad] = nextComponent; } return { ...state, ...nextComponent && { components: updatedComponents}, latestIndexLoaded: indexToLoad, isLoading: false, didReachMaximumRequests: false, ...isFinished && { // Special case when finished on first request finalIndex: indexToLoad === 0 ? -1 : indexToLoad, }, }; }); } else { attemptRetry(); } }) .catch(attemptRetry); } else { console.log( `Max total attempts reached (${MAX_TOTAL_REQUESTS})` ); setState({ isLoading: false, didReachMaximumRequests: true, }); } } else { console.log( `Max attempts per request reached ${MAX_ATTEMPTS_PER_REQUEST}` ); setState({ isLoading: false, didReachMaximumRequests: true, }); } }, [ setState, getNextComponent, initialOffset, indexToLoad, itemsPerRequest, debug, ]); useEffect(() => { if ( !isLoading && indexToLoad >= components.length ) { attempt(); } }, [isLoading, indexToLoad, attempt, components.length]); const buttonRef = useRef(null); const resetRequestsAndRetry = useCallback(() => { attemptsPerRequest.current = 0; totalRequests.current = 0; setState({ didReachMaximumRequests: false }); attempt(); }, [attempt, setState]); const advance = useCallback(() => { if (indexInView === undefined) { setState({ indexInView: 0 }); } else if ( indexInView <= components.length - 1 && ( finalIndex === undefined || indexInView < finalIndex ) ) { setState({ indexInView: indexInView + 1}); } }, [components.length, finalIndex, indexInView, setState]); useEffect(() => { // Only add observer if button is rendered if (buttonRef.current) { const observer = new IntersectionObserver(e => { if (triggerOnView && e[0].isIntersecting) { advance(); } }, { root: null, threshold: 0, }); observer.observe(buttonRef.current); return () => observer.disconnect(); } }, [triggerOnView, advance]); const renderMoreButton = () => ; if (debug) { console.log({ indexInView, componentsLength: components.length, finalIndex, hasFinalIndexBeenReached, areAllComponentsVisible, isLoading, }); } return <>
{components.slice(0, (indexInView ?? 0) + 1)}
{showMoreButton && ( wrapMoreButtonInSiteGrid ? : renderMoreButton() )}
; }