/**
* 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;