fix: Prevent infinite scroll refresh by stabilizing intersection observer - Add trigger prevention mechanism to avoid multiple API calls - Use refs to store current values and avoid stale closures - Remove observer dependencies to prevent recreation - Add proper timeout cleanup to prevent memory leaks
This commit is contained in:
parent
2e32a3c3b6
commit
4f16c859db
@ -121,6 +121,20 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const loadingRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const isTriggeringRef = useRef(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(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<PaginatedSongListProps> = 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<PaginatedSongListProps> = memo(({
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
}
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [hasMore, loading, onLoadMore]);
|
||||
}, []); // Remove dependencies to prevent recreation
|
||||
|
||||
return (
|
||||
<Flex direction="column" height="100%">
|
||||
@ -298,6 +321,11 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
||||
</Checkbox>
|
||||
<Text color="gray.400" fontSize="sm">
|
||||
{songs.length} of {totalSongs} songs • {totalDuration}
|
||||
{hasMore && songs.length > 0 && (
|
||||
<Text as="span" color="blue.400" ml={2}>
|
||||
• Scroll for more
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
@ -354,14 +382,26 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
||||
|
||||
{/* Loading indicator for infinite scroll */}
|
||||
{loading && (
|
||||
<Flex justify="center" p={4} key="loading-spinner">
|
||||
<Flex justify="center" align="center" p={6} key="loading-spinner" gap={3}>
|
||||
<Spinner size="md" color="blue.400" />
|
||||
<Text color="gray.400" fontSize="sm">
|
||||
Loading more songs...
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* Intersection observer target */}
|
||||
<div ref={loadingRef} style={{ height: '20px' }} key="intersection-target" />
|
||||
|
||||
{/* Loading more indicator (subtle) */}
|
||||
{!loading && hasMore && songs.length > 0 && (
|
||||
<Flex justify="center" p={3} key="loading-more-indicator">
|
||||
<Text color="gray.600" fontSize="xs">
|
||||
Scroll for more songs
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* End of results message */}
|
||||
{!hasMore && songs.length > 0 && (
|
||||
<Flex justify="center" p={4} key="end-message">
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user