A social knowledge tool for researchers built on ATProto
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}