/** * useSearch Hook * Search memories with debouncing */ import { useState, useCallback, useRef, useEffect } from 'react'; import { useMemoryStackClient } from './useMemoryStack'; import type { Memory, UseSearchOptions, UseSearchResult } from '../types'; /** * Hook for searching memories with debouncing * * @example * ```tsx * function SearchScreen() { * const { results, isSearching, error, search, clearResults } = useSearch({ * debounceMs: 300, * limit: 10, * }); * * return ( * * * {isSearching && } * {error && Error: {error.message}} * } * /> * * ); * } * ``` */ export function useSearch(options: UseSearchOptions = {}): UseSearchResult { const client = useMemoryStackClient(); const [results, setResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [error, setError] = useState(null); const { debounceMs = 300, limit = 10, userId, } = options; const debounceTimerRef = useRef | null>(null); const lastQueryRef = useRef(''); const isMountedRef = useRef(true); // Cleanup on unmount useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }; }, []); // Execute search const executeSearch = useCallback(async (query: string) => { if (!client || !query.trim()) { setResults([]); setIsSearching(false); return; } try { setIsSearching(true); setError(null); const response = await client.search(query, { limit, userId, }); if (!isMountedRef.current) return; // Only update if this is still the latest query if (query === lastQueryRef.current) { setResults(response.results); } } catch (err) { if (!isMountedRef.current) return; if (query === lastQueryRef.current) { setError(err instanceof Error ? err : new Error('Search failed')); setResults([]); } } finally { if (isMountedRef.current && query === lastQueryRef.current) { setIsSearching(false); } } }, [client, limit, userId]); // Debounced search const search = useCallback((query: string) => { lastQueryRef.current = query; // Clear existing timer if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } // Clear results immediately if empty query if (!query.trim()) { setResults([]); setIsSearching(false); setError(null); return; } // Set searching state immediately for UI feedback setIsSearching(true); // Debounce the actual API call debounceTimerRef.current = setTimeout(() => { executeSearch(query); }, debounceMs); }, [executeSearch, debounceMs]); // Clear results const clearResults = useCallback(() => { lastQueryRef.current = ''; setResults([]); setError(null); setIsSearching(false); if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }, []); return { results, isSearching, error, search, clearResults, }; } export default useSearch;