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,14 +224,16 @@ 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) => {
setSelectedSongs(prev => { startTransition(() => {
const newSelection = new Set(prev); setSelectedSongs(prev => {
if (newSelection.has(songId)) { const newSelection = new Set(prev);
newSelection.delete(songId); if (newSelection.has(songId)) {
} else { newSelection.delete(songId);
newSelection.add(songId); } else {
} newSelection.add(songId);
return newSelection; }
return newSelection;
});
}); });
}, []); }, []);
@ -239,14 +241,16 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
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];
setSelectedSongs(prev => { startTransition(() => {
const next = new Set(prev); setSelectedSongs(prev => {
for (let i = start; i <= end; i++) { const next = new Set(prev);
const id = songs[i]?.id; for (let i = start; i <= end; i++) {
if (!id) continue; const id = songs[i]?.id;
if (checked) next.add(id); else next.delete(id); if (!id) continue;
} if (checked) next.add(id); else next.delete(id);
return next; }
return next;
});
}); });
}, [songs]); }, [songs]);
@ -262,19 +266,18 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
}, [songs, toggleSelection, toggleSelectionRange]); }, [songs, toggleSelection, toggleSelectionRange]);
const toggleSelectAll = useCallback(() => { const toggleSelectAll = useCallback(() => {
setSelectedSongs(prev => { startTransition(() => {
const noneSelected = prev.size === 0; setSelectedSongs(prev => {
const allSelected = prev.size === songs.length && songs.length > 0; const noneSelected = prev.size === 0;
if (noneSelected) { const allSelected = prev.size === songs.length && songs.length > 0;
// Select all from empty state if (noneSelected) {
return new Set(songs.map(s => s.id));
}
if (allSelected) {
return new Set();
}
return new Set(songs.map(s => s.id)); return new Set(songs.map(s => s.id));
} });
if (allSelected) {
// Deselect all when everything is selected
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));
}); });
}, [songs]); }, [songs]);