rekordbox-viewer/packages/frontend/src/hooks/usePaginatedSongs.ts

167 lines
4.8 KiB
TypeScript

import { useState, useEffect, useCallback, useRef } from 'react';
import { api, type SongsResponse } from '../services/api';
import type { Song } from '../types/interfaces';
interface UsePaginatedSongsOptions {
pageSize?: number;
initialSearch?: string;
playlistName?: string;
}
export const usePaginatedSongs = (options: UsePaginatedSongsOptions = {}) => {
const { pageSize = 50, initialSearch = '', playlistName } = options;
const [songs, setSongs] = useState<Song[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [hasMore, setHasMore] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState(initialSearch);
const [totalSongs, setTotalSongs] = useState(0);
const [isInitialLoad, setIsInitialLoad] = useState(true);
const loadingRef = useRef(false);
const currentPlaylistRef = useRef(playlistName);
const abortControllerRef = useRef<AbortController | null>(null);
// Cleanup function to prevent memory leaks
const cleanup = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
loadingRef.current = false;
}, []);
// Load songs for a specific page
const loadPage = useCallback(async (page: number, search?: string, targetPlaylist?: string) => {
if (loadingRef.current) return;
const searchToUse = search ?? searchQuery;
const playlistToUse = targetPlaylist ?? playlistName;
// Cleanup previous request
cleanup();
// Create new abort controller for this request
abortControllerRef.current = new AbortController();
loadingRef.current = true;
setLoading(true);
setError(null);
try {
let response: SongsResponse;
if (playlistToUse && playlistToUse !== 'All Songs') {
// Load songs for specific playlist
response = await api.getPlaylistSongsPaginated(playlistToUse, page, pageSize, searchToUse);
} else {
// Load all songs
response = await api.getSongsPaginated(page, pageSize, searchToUse);
}
// Check if request was aborted
if (abortControllerRef.current?.signal.aborted) {
return;
}
if (page === 1) {
// First page - replace all songs
setSongs(response.songs);
} else {
// Subsequent pages - append songs
setSongs(prev => [...prev, ...response.songs]);
}
setHasMore(response.pagination.hasNextPage);
setTotalSongs(response.pagination.totalSongs);
setCurrentPage(page);
} catch (err) {
// Don't set error if request was aborted
if (!abortControllerRef.current?.signal.aborted) {
setError(err instanceof Error ? err.message : 'Failed to load songs');
}
} finally {
if (!abortControllerRef.current?.signal.aborted) {
setLoading(false);
loadingRef.current = false;
setIsInitialLoad(false);
}
}
}, [pageSize, cleanup]); // Remove searchQuery and playlistName from dependencies
// Load next page (for infinite scroll)
const loadNextPage = useCallback(() => {
if (!loading && hasMore && !loadingRef.current) {
loadPage(currentPage + 1);
}
}, [loading, hasMore, currentPage, loadPage]);
// Search songs with debouncing
const searchSongs = useCallback((query: string) => {
setSearchQuery(query);
// Clear songs for new search to replace them
setSongs([]);
setHasMore(true);
setCurrentPage(1);
setError(null);
loadPage(1, query);
}, [loadPage]);
// Reset to initial state
const reset = useCallback(() => {
cleanup();
setSongs([]);
setLoading(false);
setError(null);
setHasMore(true);
setCurrentPage(1);
setSearchQuery(initialSearch);
setIsInitialLoad(true);
}, [initialSearch, cleanup]);
// Initial load
useEffect(() => {
loadPage(1);
// Cleanup on unmount
return () => {
cleanup();
};
}, []);
// Handle playlist changes
useEffect(() => {
if (currentPlaylistRef.current !== playlistName) {
currentPlaylistRef.current = playlistName;
if (!isInitialLoad) {
// Clear songs for new playlist to replace them
setSongs([]);
setHasMore(true);
setCurrentPage(1);
setSearchQuery(initialSearch);
setError(null);
// Use setTimeout to avoid the dependency issue
setTimeout(() => {
loadPage(1, initialSearch, playlistName);
}, 0);
}
}
}, [playlistName, isInitialLoad, initialSearch, loadPage]);
return {
songs,
loading,
error,
hasMore,
totalSongs,
currentPage,
searchQuery,
isInitialLoad,
loadNextPage,
searchSongs,
reset,
refresh: () => loadPage(1)
};
};