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:
parent
4c63228619
commit
383f3476f0
@ -70,10 +70,14 @@ const SongItem = memo<{
|
|||||||
// Memoize the formatted duration to prevent recalculation
|
// Memoize the formatted duration to prevent recalculation
|
||||||
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
|
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
console.log('SongItem clicked:', song.title);
|
|
||||||
onSelect(song);
|
onSelect(song);
|
||||||
}, [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>) => {
|
const handleCheckboxClick = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onCheckboxToggle) {
|
if (onCheckboxToggle) {
|
||||||
@ -85,14 +89,10 @@ const SongItem = memo<{
|
|||||||
|
|
||||||
const handlePlayClick = useCallback((e: React.MouseEvent) => {
|
const handlePlayClick = useCallback((e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onPlaySong && (song.s3File?.hasS3File || song.hasMusicFile)) {
|
if (onPlaySong && hasMusicFile) {
|
||||||
onPlaySong(song);
|
onPlaySong(song);
|
||||||
}
|
}
|
||||||
}, [onPlaySong, song]);
|
}, [onPlaySong, hasMusicFile, song]);
|
||||||
|
|
||||||
const hasMusicFile = (song: Song): boolean => {
|
|
||||||
return song.s3File?.hasS3File || song.hasMusicFile || false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@ -138,7 +138,7 @@ const SongItem = memo<{
|
|||||||
{song.averageBpm} BPM
|
{song.averageBpm} BPM
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{hasMusicFile(song) && onPlaySong && (
|
{hasMusicFile && onPlaySong && (
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Play song"
|
aria-label="Play song"
|
||||||
icon={<FiPlay />}
|
icon={<FiPlay />}
|
||||||
@ -156,6 +156,28 @@ const SongItem = memo<{
|
|||||||
|
|
||||||
SongItem.displayName = 'SongItem';
|
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(({
|
export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
||||||
songs,
|
songs,
|
||||||
onAddToPlaylist,
|
onAddToPlaylist,
|
||||||
@ -303,6 +325,54 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
onSongSelect(song);
|
onSongSelect(song);
|
||||||
}, [onSongSelect]);
|
}, [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
|
// Memoized search handler with debouncing
|
||||||
// Search handled inline via localSearchQuery effect
|
// Search handled inline via localSearchQuery effect
|
||||||
|
|
||||||
@ -591,7 +661,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
{songs.map((song, index) => {
|
{songs.map((song, index) => {
|
||||||
const allowReorder = Boolean(onReorder && currentPlaylist);
|
const allowReorder = Boolean(onReorder && currentPlaylist);
|
||||||
return (
|
return (
|
||||||
<SongItem
|
<OptimizedSongItem
|
||||||
key={song.id}
|
key={song.id}
|
||||||
song={song}
|
song={song}
|
||||||
isSelected={selectedSongs.has(song.id)}
|
isSelected={selectedSongs.has(song.id)}
|
||||||
@ -604,44 +674,9 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
onPlaySong={onPlaySong}
|
onPlaySong={onPlaySong}
|
||||||
showDropIndicatorTop={dragHoverIndex === index}
|
showDropIndicatorTop={dragHoverIndex === index}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onRowDragOver={allowReorder ? ((e: React.DragEvent) => {
|
onRowDragOver={allowReorder ? createRowDragOver(index) : undefined}
|
||||||
if (!onReorder || !currentPlaylist) return;
|
onRowDrop={allowReorder ? createRowDrop(index) : undefined}
|
||||||
e.preventDefault();
|
onRowDragStartCapture={allowReorder ? createRowDragStartCapture(song) : undefined}
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user