// hooks/useAlgoliaSearch.ts
import { useState, useEffect, useRef, useCallback } from 'react';
import { algoliasearch } from 'algoliasearch';
import type { PokemonData } from '../types/pokemon';
const ALGOLIA_APP_ID = import.meta.env.VITE_ALGOLIA_APP_ID || 'demo_app_id';
const ALGOLIA_API_KEY = import.meta.env.VITE_ALGOLIA_API_KEY || 'demo_api_key';
const POKEMON_INDEX = 'pokemon';
interface SearchResult {
hits: PokemonData[];
nbHits: number;
processingTimeMS: number;
query: string;
}
interface SearchState {
results: SearchResult | null;
isLoading: boolean;
error: string | null;
hasSearched: boolean;
}
interface SearchCache {
[query: string]: SearchResult;
}
export const useAlgoliaSearch = () => {
const [state, setState] = useState<SearchState>({
results: null,
isLoading: false,
error: null,
hasSearched: false
});
const searchClient = useRef(algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_KEY));
const abortController = useRef<AbortController | null>(null);
const debounceTimer = useRef<number | undefined>(undefined);
const cache = useRef<SearchCache>({});
const lastQuery = useRef<string>('');
// Cleanup function
const cleanup = useCallback(() => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
debounceTimer.current = undefined;
}
if (abortController.current) {
abortController.current.abort();
abortController.current = null;
}
}, []);
// Cleanup on unmount
useEffect(() => {
return cleanup;
}, [cleanup]);
const performSearch = useCallback(async (query: string) => {
const trimmedQuery = query.trim().toLowerCase();
// Don't search if query hasn't changed
if (trimmedQuery === lastQuery.current && state.results) {
return;
}
lastQuery.current = trimmedQuery;
// Check cache first
if (cache.current[trimmedQuery]) {
setState(prev => ({
...prev,
results: cache.current[trimmedQuery],
isLoading: false,
error: null,
hasSearched: true
}));
return;
}
// Cancel any ongoing search
if (abortController.current) {
abortController.current.abort();
}
// Create new abort controller for this search
abortController.current = new AbortController();
setState(prev => ({
...prev,
isLoading: true,
error: null
}));
try {
const startTime = Date.now();
const searchResponse = await searchClient.current.searchSingleIndex({
indexName: POKEMON_INDEX,
searchParams: {
query: query.trim(),
hitsPerPage: 20,
typoTolerance: 'min',
minWordSizefor1Typo: 4,
minWordSizefor2Typos: 8,
attributesToHighlight: ['name'],
highlightPreTag: '<mark class="text-yellow-400 bg-yellow-400/20">',
highlightPostTag: '</mark>',
}
});
const processingTime = Date.now() - startTime;
const result: SearchResult = {
hits: searchResponse.hits as PokemonData[],
nbHits: searchResponse.nbHits || 0,
processingTimeMS: processingTime,
query: trimmedQuery
};
// Cache the result with normalized key
cache.current[trimmedQuery] = result;
setState(prev => ({
...prev,
results: result,
isLoading: false,
error: null,
hasSearched: true
}));
} catch (error: unknown) {
// Don't update state if the request was aborted
if (error instanceof Error && error.name === 'AbortError') {
return;
}
console.error('[useAlgoliaSearch] Search error:', error);
setState(prev => ({
...prev,
results: null,
isLoading: false,
error: error instanceof Error ? error.message : 'Search failed',
hasSearched: true
}));
} finally {
abortController.current = null;
}
}, [state.results]);
const search = useCallback((query: string, debounceMs: number = 150) => {
// Clear existing timer
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
// Handle empty query
if (!query.trim()) {
setState(prev => ({
...prev,
results: null,
isLoading: false,
error: null,
hasSearched: false
}));
return;
}
// Debounce the search
debounceTimer.current = window.setTimeout(() => {
performSearch(query.trim());
}, debounceMs);
}, [performSearch]);
const clearSearch = useCallback(() => {
cleanup();
setState({
results: null,
isLoading: false,
error: null,
hasSearched: false
});
lastQuery.current = '';
}, [cleanup]);
const clearCache = useCallback(() => {
cache.current = {};
}, []);
return {
// State
results: state.results,
isLoading: state.isLoading,
error: state.error,
hasSearched: state.hasSearched,
// Actions
search,
clearSearch,
clearCache,
// Utils
getCacheSize: () => Object.keys(cache.current).length,
getLastQuery: () => lastQuery.current
};
};