From ff589b73c54975b6cfbccfd73b2a054675cc39b5 Mon Sep 17 00:00:00 2001 From: senke Date: Sun, 11 Jan 2026 16:49:07 +0100 Subject: [PATCH] data-flow: add debounce to LibraryPage search and fix race condition - Completed Actions 2.4.1.1, 2.4.1.2, and 2.4.1.4 - Action 2.4.1.1: Verified custom useDebounce hook exists (no external package needed) - Action 2.4.1.2: Added useDebounce hook with 300ms delay to LibraryPage search - Action 2.4.1.4: Fixed race condition by using debouncedSearchTerm for page reset - Search now fires 300ms after typing stops, reducing API calls - Page reset now waits for debounce to complete, preventing race conditions --- EXHAUSTIVE_TODO_LIST.md | 24 +++++++++---------- .../features/library/pages/LibraryPage.tsx | 17 ++++++++----- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/EXHAUSTIVE_TODO_LIST.md b/EXHAUSTIVE_TODO_LIST.md index 8ba33f3d2..531cb4fc6 100644 --- a/EXHAUSTIVE_TODO_LIST.md +++ b/EXHAUSTIVE_TODO_LIST.md @@ -632,32 +632,32 @@ Critical path dependencies: ### Sub-Epic 2.4: Request Debouncing 🟢 #### Task 2.4.1: Add Debounce to Search Inputs -- [ ] **Action 2.4.1.1**: Install `use-debounce` or implement custom hook +- [x] **Action 2.4.1.1**: Install `use-debounce` or implement custom hook - **Scope**: `apps/web/package.json` - Add dependency (if using library) - - **Dependencies**: None + - **Dependencies**: None ✅ - **Risk**: LOW - - **Validation**: Package installed - - **Rollback**: Remove from package.json + - **Validation**: ✅ Custom `useDebounce` hook already exists at `apps/web/src/hooks/useDebounce.ts` with tests. No external package needed. + - **Rollback**: N/A (custom implementation) -- [ ] **Action 2.4.1.2**: Add debounce to LibraryPage search +- [x] **Action 2.4.1.2**: Add debounce to LibraryPage search - **Scope**: `apps/web/src/features/library/pages/LibraryPage.tsx:322-327` - Debounce `setSearchTerm` - - **Dependencies**: Action 2.4.1.1 complete + - **Dependencies**: Action 2.4.1.1 complete ✅ - **Risk**: LOW - - **Validation**: Search fires 300ms after typing stops - - **Rollback**: Remove debounce + - **Validation**: ✅ Added `useDebounce` hook with 300ms delay. Search term is debounced before being used in queryParams and queryKey. Search fires 300ms after typing stops. + - **Rollback**: Remove debounce hook usage - [ ] **Action 2.4.1.3**: Add debounce to all search inputs - **Scope**: Audit all search inputs, add debounce - - **Dependencies**: Action 2.4.1.2 complete + - **Dependencies**: Action 2.4.1.2 complete ✅ - **Risk**: LOW - **Validation**: All searches debounced - **Rollback**: Remove debounce from each -- [ ] **Action 2.4.1.4**: Fix race condition in LibraryPage search/page reset +- [x] **Action 2.4.1.4**: Fix race condition in LibraryPage search/page reset - **Scope**: `apps/web/src/features/library/pages/LibraryPage.tsx:116-120` - Use debounced search term for page reset - - **Dependencies**: Action 2.4.1.2 complete + - **Dependencies**: Action 2.4.1.2 complete ✅ - **Risk**: LOW - - **Validation**: Page resets only after debounce completes + - **Validation**: ✅ Updated useEffect to use `debouncedSearchTerm` instead of `searchTerm` for page reset. Page resets only after debounce completes, fixing race condition. - **Rollback**: Restore original useEffect --- diff --git a/apps/web/src/features/library/pages/LibraryPage.tsx b/apps/web/src/features/library/pages/LibraryPage.tsx index 6cf60524a..adc8a8821 100644 --- a/apps/web/src/features/library/pages/LibraryPage.tsx +++ b/apps/web/src/features/library/pages/LibraryPage.tsx @@ -1,4 +1,5 @@ import { useState, useMemo, useEffect } from 'react'; +import { useDebounce } from '@/hooks/useDebounce'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { usePlaylists, @@ -70,6 +71,8 @@ export default function LibraryPagePremium() { const [viewMode, setViewMode] = useState('grid'); const [searchTerm, setSearchTerm] = useState(''); + // Action 2.4.1.2: Debounce search term to reduce API calls + const debouncedSearchTerm = useDebounce(searchTerm, 300); const [genreFilter, setGenreFilter] = useState(''); const [formatFilter, setFormatFilter] = useState(''); const [sortBy, setSortBy] = useState('created_at'); @@ -91,9 +94,10 @@ export default function LibraryPagePremium() { if (formatFilter) { queryParams.format = formatFilter; } - if (searchTerm.trim()) { - queryParams.search = searchTerm.trim(); - } + // Use debounced search term for API calls + if (debouncedSearchTerm.trim()) { + queryParams.search = debouncedSearchTerm.trim(); + } const { data: tracksData, @@ -101,7 +105,7 @@ export default function LibraryPagePremium() { isError: isTracksError, error: tracksError, } = useQuery({ - queryKey: ['tracks', 'library', queryParams, searchTerm], + queryKey: ['tracks', 'library', queryParams, debouncedSearchTerm], queryFn: () => getTracks(page, limit, queryParams), }); @@ -113,11 +117,12 @@ export default function LibraryPagePremium() { return tracksData.tracks; }, [tracksData?.tracks]); + // Action 2.4.1.4: Use debounced search term for page reset to fix race condition useEffect(() => { - if (searchTerm.trim() && page !== 1) { + if (debouncedSearchTerm.trim() && page !== 1) { setPage(1); } - }, [searchTerm]); + }, [debouncedSearchTerm, page]); const genres = Array.from( new Set(