perf(frontend): use startTransition for selection updates to keep UI responsive on large sets

This commit is contained in:
Geert Rademakes 2025-08-13 16:03:26 +02:00
parent 1d290bdfa6
commit 017ba31d83

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, useCallback, useMemo, memo } from 'react'; import React, { useState, useRef, useEffect, useCallback, useMemo, memo, startTransition } from 'react';
import { import {
Box, Box,
Flex, Flex,
@ -224,6 +224,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
// const allPlaylists = useMemo(() => getAllPlaylists(playlists), [playlists, getAllPlaylists]); // const allPlaylists = useMemo(() => getAllPlaylists(playlists), [playlists, getAllPlaylists]);
const toggleSelection = useCallback((songId: string) => { const toggleSelection = useCallback((songId: string) => {
startTransition(() => {
setSelectedSongs(prev => { setSelectedSongs(prev => {
const newSelection = new Set(prev); const newSelection = new Set(prev);
if (newSelection.has(songId)) { if (newSelection.has(songId)) {
@ -233,12 +234,14 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
} }
return newSelection; return newSelection;
}); });
});
}, []); }, []);
// Range selection using shift-click between last selected and current // Range selection using shift-click between last selected and current
const toggleSelectionRange = useCallback((fromIndex: number, toIndex: number, checked: boolean) => { const toggleSelectionRange = useCallback((fromIndex: number, toIndex: number, checked: boolean) => {
if (fromIndex === null || toIndex === null) return; if (fromIndex === null || toIndex === null) return;
const [start, end] = fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex]; const [start, end] = fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex];
startTransition(() => {
setSelectedSongs(prev => { setSelectedSongs(prev => {
const next = new Set(prev); const next = new Set(prev);
for (let i = start; i <= end; i++) { for (let i = start; i <= end; i++) {
@ -248,6 +251,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
} }
return next; return next;
}); });
});
}, [songs]); }, [songs]);
const handleCheckboxToggle = useCallback((index: number, checked: boolean, shift: boolean) => { const handleCheckboxToggle = useCallback((index: number, checked: boolean, shift: boolean) => {
@ -262,20 +266,19 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
}, [songs, toggleSelection, toggleSelectionRange]); }, [songs, toggleSelection, toggleSelectionRange]);
const toggleSelectAll = useCallback(() => { const toggleSelectAll = useCallback(() => {
startTransition(() => {
setSelectedSongs(prev => { setSelectedSongs(prev => {
const noneSelected = prev.size === 0; const noneSelected = prev.size === 0;
const allSelected = prev.size === songs.length && songs.length > 0; const allSelected = prev.size === songs.length && songs.length > 0;
if (noneSelected) { if (noneSelected) {
// Select all from empty state
return new Set(songs.map(s => s.id)); return new Set(songs.map(s => s.id));
} }
if (allSelected) { if (allSelected) {
// Deselect all when everything is selected
return new Set(); return new Set();
} }
// Mixed/some selected: clear first, then select all (single state update reflects final state)
return new Set(songs.map(s => s.id)); return new Set(songs.map(s => s.id));
}); });
});
}, [songs]); }, [songs]);
const handleBulkAddToPlaylist = useCallback((playlistName: string) => { const handleBulkAddToPlaylist = useCallback((playlistName: string) => {