A social knowledge tool for researchers built on ATProto
at development 3.5 kB view raw
1import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; 2import { ApiClient } from '@/api-client/ApiClient'; 3import type { GetCollectionsResponse } from '@/api-client/types'; 4 5interface UseCollectionSearchProps { 6 apiClient: ApiClient; 7 initialLoad?: boolean; 8 debounceMs?: number; 9} 10 11export function useCollectionSearch({ 12 apiClient, 13 initialLoad = true, 14 debounceMs = 300, 15}: UseCollectionSearchProps) { 16 const [collections, setCollections] = useState< 17 GetCollectionsResponse['collections'] 18 >([]); 19 const [loading, setLoading] = useState(false); 20 const [searchText, setSearchText] = useState(''); 21 const [hasInitialized, setHasInitialized] = useState(false); 22 const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null); 23 24 // Memoized search parameters to avoid unnecessary API calls 25 const searchParams = useMemo( 26 () => ({ 27 limit: 20, 28 sortBy: 'updatedAt' as const, 29 sortOrder: 'desc' as const, 30 }), 31 [], 32 ); 33 34 // Memoized load function that only changes when apiClient changes 35 const loadCollections = useCallback( 36 async (search?: string) => { 37 setLoading(true); 38 try { 39 const response = await apiClient.getMyCollections({ 40 ...searchParams, 41 searchText: search || undefined, 42 }); 43 setCollections(response.collections); 44 } catch (error) { 45 console.error('Error loading collections:', error); 46 // Don't clear collections on error, keep showing previous results 47 } finally { 48 setLoading(false); 49 } 50 }, 51 [apiClient, searchParams], 52 ); 53 54 // Initial load effect - only runs once when component mounts 55 useEffect(() => { 56 if (initialLoad && !hasInitialized) { 57 loadCollections(); 58 setHasInitialized(true); 59 } 60 }, [initialLoad, hasInitialized, loadCollections]); 61 62 // Debounced search effect - triggers when searchText changes 63 useEffect(() => { 64 // Clear existing timeout 65 if (debounceTimeoutRef.current) { 66 clearTimeout(debounceTimeoutRef.current); 67 } 68 69 // Only set up debounced search if we've initialized 70 if (hasInitialized) { 71 debounceTimeoutRef.current = setTimeout(() => { 72 const trimmedSearch = searchText.trim(); 73 loadCollections(trimmedSearch || undefined); 74 }, debounceMs); 75 } 76 77 // Cleanup timeout on unmount or when dependencies change 78 return () => { 79 if (debounceTimeoutRef.current) { 80 clearTimeout(debounceTimeoutRef.current); 81 } 82 }; 83 }, [searchText, loadCollections, hasInitialized, debounceMs]); 84 85 // Manual search function (kept for backward compatibility) 86 const handleSearch = useCallback(() => { 87 // Clear any pending debounced search 88 if (debounceTimeoutRef.current) { 89 clearTimeout(debounceTimeoutRef.current); 90 } 91 92 const trimmedSearch = searchText.trim(); 93 loadCollections(trimmedSearch || undefined); 94 }, [searchText, loadCollections]); 95 96 // Handle search input changes 97 const handleSearchTextChange = useCallback((value: string) => { 98 setSearchText(value); 99 }, []); 100 101 // Handle search on Enter key press (immediate search, no debounce) 102 const handleSearchKeyPress = useCallback( 103 (e: React.KeyboardEvent) => { 104 if (e.key === 'Enter') { 105 handleSearch(); 106 } 107 }, 108 [handleSearch], 109 ); 110 111 return { 112 collections, 113 loading, 114 searchText, 115 setSearchText: handleSearchTextChange, 116 handleSearch, 117 handleSearchKeyPress, 118 loadCollections, 119 }; 120}