Optimize PaginatedSongList performance to prevent re-renders

- Memoized drag handlers to prevent inline function recreation
- Added custom React.memo comparison for SongItem components
- Optimized hasMusicFile calculation with useMemo
- Removed debug console.log statements
- Fixed inline function creation in song mapping

This should significantly reduce re-renders and improve song selection performance.
This commit is contained in:
Geert Rademakes 2025-09-19 11:05:16 +02:00
parent 4c63228619
commit 383f3476f0

View File

@ -70,10 +70,14 @@ const SongItem = memo<{
// Memoize the formatted duration to prevent recalculation
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
const handleClick = useCallback(() => {
console.log('SongItem clicked:', song.title);
onSelect(song);
}, [onSelect, song]);
const hasMusicFile = useMemo(() =>
song.s3File?.hasS3File || song.hasMusicFile || false,
[song.s3File?.hasS3File, song.hasMusicFile]
);
const handleCheckboxClick = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
if (onCheckboxToggle) {
@ -85,14 +89,10 @@ const SongItem = memo<{
const handlePlayClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
if (onPlaySong && (song.s3File?.hasS3File || song.hasMusicFile)) {
if (onPlaySong && hasMusicFile) {
onPlaySong(song);
}
}, [onPlaySong, song]);
const hasMusicFile = (song: Song): boolean => {
return song.s3File?.hasS3File || song.hasMusicFile || false;
};
}, [onPlaySong, hasMusicFile, song]);
return (
<Flex
@ -138,7 +138,7 @@ const SongItem = memo<{
{song.averageBpm} BPM
</Text>
</Box>
{hasMusicFile(song) && onPlaySong && (
{hasMusicFile && onPlaySong && (
<IconButton
aria-label="Play song"
icon={<FiPlay />}
@ -156,6 +156,28 @@ const SongItem = memo<{
SongItem.displayName = 'SongItem';
// Custom comparison function to prevent unnecessary re-renders
const areEqual = (prevProps: any, nextProps: any) => {
return (
prevProps.song.id === nextProps.song.id &&
prevProps.isSelected === nextProps.isSelected &&
prevProps.isHighlighted === nextProps.isHighlighted &&
prevProps.showCheckbox === nextProps.showCheckbox &&
prevProps.showDropIndicatorTop === nextProps.showDropIndicatorTop &&
prevProps.index === nextProps.index &&
prevProps.song.title === nextProps.song.title &&
prevProps.song.artist === nextProps.song.artist &&
prevProps.song.totalTime === nextProps.song.totalTime &&
prevProps.song.tonality === nextProps.song.tonality &&
prevProps.song.averageBpm === nextProps.song.averageBpm &&
prevProps.song.s3File?.hasS3File === nextProps.song.s3File?.hasS3File &&
prevProps.song.hasMusicFile === nextProps.song.hasMusicFile
);
};
// Apply custom comparison to SongItem
const OptimizedSongItem = memo(SongItem, areEqual);
export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
songs,
onAddToPlaylist,
@ -303,6 +325,54 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
onSongSelect(song);
}, [onSongSelect]);
// Memoized drag handlers to prevent re-renders
const createRowDragOver = useCallback((index: number) => {
if (!onReorder || !currentPlaylist) return undefined;
return (e: React.DragEvent) => {
e.preventDefault();
setDragHoverIndex(index);
};
}, [onReorder, currentPlaylist]);
const createRowDrop = useCallback((index: number) => {
if (!onReorder || !currentPlaylist) return undefined;
return async (e: React.DragEvent) => {
e.preventDefault();
const fromId = e.dataTransfer.getData('text/song-id');
const multiJson = e.dataTransfer.getData('application/json');
let multiIds: string[] | null = null;
if (multiJson) {
try {
const parsed = JSON.parse(multiJson);
if (parsed && parsed.type === 'songs' && Array.isArray(parsed.songIds)) {
multiIds = parsed.songIds as string[];
}
} catch {}
}
if (!fromId && !multiIds) return;
const toId = songs[index]?.id;
if (!toId) return;
if (multiIds && multiIds.length > 0) {
await api.moveTracksInPlaylist(currentPlaylist, multiIds, toId);
} else {
await api.moveTrackInPlaylist(currentPlaylist, fromId!, toId);
}
await onReorder(songs.map(s => s.id));
setDragHoverIndex(null);
setIsReorderDragging(false);
};
}, [onReorder, currentPlaylist, songs]);
const createRowDragStartCapture = useCallback((song: Song) => {
if (!currentPlaylist) return undefined;
return (e: React.DragEvent) => {
e.dataTransfer.setData('text/song-id', song.id);
try { e.dataTransfer.effectAllowed = 'move'; } catch {}
try { e.dataTransfer.dropEffect = 'move'; } catch {}
setIsReorderDragging(true);
};
}, [currentPlaylist]);
// Memoized search handler with debouncing
// Search handled inline via localSearchQuery effect
@ -591,7 +661,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
{songs.map((song, index) => {
const allowReorder = Boolean(onReorder && currentPlaylist);
return (
<SongItem
<OptimizedSongItem
key={song.id}
song={song}
isSelected={selectedSongs.has(song.id)}
@ -604,44 +674,9 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
onPlaySong={onPlaySong}
showDropIndicatorTop={dragHoverIndex === index}
onDragStart={handleDragStart}
onRowDragOver={allowReorder ? ((e: React.DragEvent) => {
if (!onReorder || !currentPlaylist) return;
e.preventDefault();
setDragHoverIndex(index);
}) : undefined}
onRowDrop={allowReorder ? (async (e: React.DragEvent) => {
if (!onReorder || !currentPlaylist) return;
e.preventDefault();
const fromId = e.dataTransfer.getData('text/song-id');
const multiJson = e.dataTransfer.getData('application/json');
let multiIds: string[] | null = null;
if (multiJson) {
try {
const parsed = JSON.parse(multiJson);
if (parsed && parsed.type === 'songs' && Array.isArray(parsed.songIds)) {
multiIds = parsed.songIds as string[];
}
} catch {}
}
if (!fromId && !multiIds) return;
const toId = songs[index]?.id;
if (!toId) return;
if (multiIds && multiIds.length > 0) {
await api.moveTracksInPlaylist(currentPlaylist, multiIds, toId);
} else {
await api.moveTrackInPlaylist(currentPlaylist, fromId!, toId);
}
await onReorder(songs.map(s => s.id));
setDragHoverIndex(null);
setIsReorderDragging(false);
}) : undefined}
onRowDragStartCapture={allowReorder ? ((e: React.DragEvent) => {
if (!currentPlaylist) return;
e.dataTransfer.setData('text/song-id', song.id);
try { e.dataTransfer.effectAllowed = 'move'; } catch {}
try { e.dataTransfer.dropEffect = 'move'; } catch {}
setIsReorderDragging(true);
}) : undefined}
onRowDragOver={allowReorder ? createRowDragOver(index) : undefined}
onRowDrop={allowReorder ? createRowDrop(index) : undefined}
onRowDragStartCapture={allowReorder ? createRowDragStartCapture(song) : undefined}
/>
);
})}