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 observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
const loadingRef = useRef<HTMLDivElement>(null);
|
const loadingRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollContainerRef = 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
|
// Debounce search to prevent excessive API calls
|
||||||
const debouncedSearchQuery = useDebounce(localSearchQuery, 300);
|
const debouncedSearchQuery = useDebounce(localSearchQuery, 300);
|
||||||
@ -230,8 +244,14 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
if (loadingRef.current) {
|
if (loadingRef.current) {
|
||||||
observerRef.current = new IntersectionObserver(
|
observerRef.current = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (entries[0].isIntersecting && hasMore && !loading) {
|
// Use current values from refs to avoid stale closure
|
||||||
onLoadMore();
|
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) {
|
if (observerRef.current) {
|
||||||
observerRef.current.disconnect();
|
observerRef.current.disconnect();
|
||||||
}
|
}
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [hasMore, loading, onLoadMore]);
|
}, []); // Remove dependencies to prevent recreation
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" height="100%">
|
<Flex direction="column" height="100%">
|
||||||
@ -298,6 +321,11 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Text color="gray.400" fontSize="sm">
|
<Text color="gray.400" fontSize="sm">
|
||||||
{songs.length} of {totalSongs} songs • {totalDuration}
|
{songs.length} of {totalSongs} songs • {totalDuration}
|
||||||
|
{hasMore && songs.length > 0 && (
|
||||||
|
<Text as="span" color="blue.400" ml={2}>
|
||||||
|
• Scroll for more
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
@ -354,14 +382,26 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
|
|
||||||
{/* Loading indicator for infinite scroll */}
|
{/* Loading indicator for infinite scroll */}
|
||||||
{loading && (
|
{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" />
|
<Spinner size="md" color="blue.400" />
|
||||||
|
<Text color="gray.400" fontSize="sm">
|
||||||
|
Loading more songs...
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Intersection observer target */}
|
{/* Intersection observer target */}
|
||||||
<div ref={loadingRef} style={{ height: '20px' }} key="intersection-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 */}
|
{/* End of results message */}
|
||||||
{!hasMore && songs.length > 0 && (
|
{!hasMore && songs.length > 0 && (
|
||||||
<Flex justify="center" p={4} key="end-message">
|
<Flex justify="center" p={4} key="end-message">
|
||||||
|
|||||||
@ -100,7 +100,8 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
|
|||||||
// Search songs with debouncing
|
// Search songs with debouncing
|
||||||
const searchSongs = useCallback((query: string) => {
|
const searchSongs = useCallback((query: string) => {
|
||||||
setSearchQuery(query);
|
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);
|
setHasMore(true);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setError(null);
|
setError(null);
|
||||||
@ -134,7 +135,8 @@ export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
|
|||||||
if (currentPlaylistRef.current !== playlistName) {
|
if (currentPlaylistRef.current !== playlistName) {
|
||||||
currentPlaylistRef.current = playlistName;
|
currentPlaylistRef.current = playlistName;
|
||||||
if (!isInitialLoad) {
|
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);
|
setHasMore(true);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
setSearchQuery(initialSearch);
|
setSearchQuery(initialSearch);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user