diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 112ad0c..ae98be4 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -121,6 +121,20 @@ export const PaginatedSongList: React.FC = memo(({ const observerRef = useRef(null); const loadingRef = useRef(null); const scrollContainerRef = useRef(null); + const isTriggeringRef = useRef(false); + const timeoutRef = useRef(null); + + // Store current values in refs to avoid stale closures + const hasMoreRef = useRef(hasMore); + const loadingRef_state = useRef(loading); + const onLoadMoreRef = useRef(onLoadMore); + + // Update refs when props change + useEffect(() => { + hasMoreRef.current = hasMore; + loadingRef_state.current = loading; + onLoadMoreRef.current = onLoadMore; + }, [hasMore, loading, onLoadMore]); // Debounce search to prevent excessive API calls const debouncedSearchQuery = useDebounce(localSearchQuery, 300); @@ -230,8 +244,14 @@ export const PaginatedSongList: React.FC = memo(({ if (loadingRef.current) { observerRef.current = new IntersectionObserver( (entries) => { - if (entries[0].isIntersecting && hasMore && !loading) { - onLoadMore(); + // Use current values from refs to avoid stale closure + if (entries[0].isIntersecting && hasMoreRef.current && !loadingRef_state.current && !isTriggeringRef.current) { + isTriggeringRef.current = true; + onLoadMoreRef.current(); + // Reset the flag after a short delay to prevent multiple triggers + timeoutRef.current = setTimeout(() => { + isTriggeringRef.current = false; + }, 1000); } }, { @@ -247,8 +267,11 @@ export const PaginatedSongList: React.FC = memo(({ if (observerRef.current) { observerRef.current.disconnect(); } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } }; - }, [hasMore, loading, onLoadMore]); + }, []); // Remove dependencies to prevent recreation return ( @@ -298,6 +321,11 @@ export const PaginatedSongList: React.FC = memo(({ {songs.length} of {totalSongs} songs • {totalDuration} + {hasMore && songs.length > 0 && ( + + • Scroll for more + + )} @@ -354,14 +382,26 @@ export const PaginatedSongList: React.FC = memo(({ {/* Loading indicator for infinite scroll */} {loading && ( - + + + Loading more songs... + )} {/* Intersection observer target */}
+ {/* Loading more indicator (subtle) */} + {!loading && hasMore && songs.length > 0 && ( + + + Scroll for more songs + + + )} + {/* End of results message */} {!hasMore && songs.length > 0 && ( diff --git a/packages/frontend/src/hooks/usePaginatedSongs.ts b/packages/frontend/src/hooks/usePaginatedSongs.ts index 76f0cd1..36cbeab 100644 --- a/packages/frontend/src/hooks/usePaginatedSongs.ts +++ b/packages/frontend/src/hooks/usePaginatedSongs.ts @@ -100,7 +100,8 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => { // Search songs with debouncing const searchSongs = useCallback((query: string) => { setSearchQuery(query); - // Don't clear songs immediately - let the new search results replace them + // Clear songs for new search to replace them + setSongs([]); setHasMore(true); setCurrentPage(1); setError(null); @@ -134,7 +135,8 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => { if (currentPlaylistRef.current !== playlistName) { currentPlaylistRef.current = playlistName; if (!isInitialLoad) { - // Don't clear songs immediately - let the new playlist results replace them + // Clear songs for new playlist to replace them + setSongs([]); setHasMore(true); setCurrentPage(1); setSearchQuery(initialSearch);