React useSearch Hook – Build a Smarter Search System in Your App


Never miss an update
Subscribe to receive news and special offers.
By subscribing you agree to our Privacy Policy.
React useSearch Hook – Build a Smarter Search System in Your App
Have you ever used a search feature that only works if you type the exact word? Or one that shows too many results with no way to narrow them down? These are common problems with basic search implementations.
What most developers do:
// Basic search that most developers implement
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);This simple approach has several limitations:
The useSearch hook we'll build solves all these problems in one reusable package. Think of it as a Swiss Army knife for search functionality in your React apps.
Here's how easy it is to use our hook:
// Import the hook
import { useSearch } from './hooks/useSearch';
function ProductList() {
// Your data and search state
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState(null);
// Use the hook with your data
const {
results, // Current page of filtered items
total, // Total number of matches
currentPage, // Current page number
totalPages, // Total number of pages
setCurrentPage // Function to change page
} = useSearch({
data: products, // Your data array
query: searchQuery, // Search text
filters: { category: selectedCategory }, // Any filters you want
keys: ['name', 'description', 'tags'], // Fields to search in
itemsPerPage: 10 // Items per page
});
return (
<div>
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search products..."
/>
{/* Show results count */}
<p>Found {total} products</p>
{/* Display results */}
{results.map(product => (
<ProductCard key={product.id} product={product} />
))}
{/* Pagination controls */}
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</div>
);
}Let's break down how to build this powerful hook:
import { useMemo, useState } from 'react';
// Define the hook's parameters
type UseSearchProps<T> = {
data: T[]; // Your data array
query: string; // Search text
filters?: Record<string, any>; // Optional filters
keys?: (keyof T)[]; // Fields to search in
itemsPerPage?: number; // Items per page
};
export function useSearch<T>({
data,
query,
filters = {},
keys = [],
itemsPerPage = 10
}: UseSearchProps<T>) {
// Hook implementation will go here
}// Inside the useSearch hook:
// Store current page
const [currentPage, setCurrentPage] = useState(1);
// Memoize filtered results to avoid recalculating on every render
const filtered = useMemo(() => {
// Start with all data
let results = [...data];
// Apply filters first (this is faster than text search)
if (filters && Object.keys(filters).length > 0) {
Object.entries(filters).forEach(([key, value]) => {
// Only apply filter if value isn't null/undefined
if (value !== null && value !== undefined) {
results = results.filter(item => {
// Handle array values (like tags)
if (Array.isArray(item[key as keyof T])) {
return (item[key as keyof T] as any[]).includes(value);
}
// Handle regular values
return item[key as keyof T] === value;
});
}
});
}
// Then apply text search if query exists
if (query.trim()) {
const normalizedQuery = query.toLowerCase().trim();
results = results.filter(item => {
// Search in all specified keys
return keys.some(key => {
const value = item[key];
// Skip if value doesn't exist
if (value === null || value === undefined) return false;
// Handle arrays (like tags)
if (Array.isArray(value)) {
return value.some(v =>
String(v).toLowerCase().includes(normalizedQuery)
);
}
// Handle regular values
return String(value).toLowerCase().includes(normalizedQuery);
});
});
}
return results;
}, [data, query, filters, keys]); // Only recalculate when these values change
// Calculate pagination values
const totalItems = filtered.length;
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
// Ensure current page is valid
useEffect(() => {
if (currentPage > totalPages) {
setCurrentPage(1);
}
}, [totalPages, currentPage]);
// Get current page of results
const paginatedResults = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
return filtered.slice(startIndex, startIndex + itemsPerPage);
}, [filtered, currentPage, itemsPerPage]);
// Return everything the component needs
return {
results: paginatedResults,
total: totalItems,
currentPage,
totalPages,
setCurrentPage,
};Regular search breaks when users make typos. Let's add fuzzy search to fix that:
// A simple function to calculate how similar two strings are
function calculateSimilarity(str1: string, str2: string): number {
// Convert strings to sets of 3-character sequences
const createTrigrams = (text: string) => {
const trigrams = new Set<string>();
for (let i = 0; i < text.length - 2; i++) {
trigrams.add(text.substring(i, i + 3));
}
return trigrams;
};
// Get trigrams for both strings
const set1 = createTrigrams(str1);
const set2 = createTrigrams(str2);
// Calculate intersection
let matchCount = 0;
set1.forEach(trigram => {
if (set2.has(trigram)) matchCount++;
});
// Calculate similarity score (0 to 1)
return matchCount / Math.max(set1.size, set2.size, 1);
}
// Then in the search logic:
return keys.some(key => {
const value = String(item[key] || '').toLowerCase();
// Use exact match first
if (value.includes(normalizedQuery)) return true;
// Fall back to fuzzy match if no exact match
return calculateSimilarity(value, normalizedQuery) > 0.3; // Adjust threshold as needed
});Our useSearch hook is perfect for:
E-commerce product catalogs: Let users search and filter products by category, price range, ratings, etc.
Content management: Search articles by title, content, author, or tags with pagination for large collections.
User directories: Find users by name, role, or department with smart filtering.
Documentation sites: Search through docs with fuzzy matching to handle typos.
Here are some ways to enhance the hook even more:
// Add to props
type SortDirection = 'asc' | 'desc';
sortBy?: keyof T;
sortDirection?: SortDirection;
// Add to filtering logic
if (sortBy) {
filtered.sort((a, b) => {
const valueA = a[sortBy];
const valueB = b[sortBy];
// Handle string comparison
if (typeof valueA === 'string' && typeof valueB === 'string') {
return sortDirection === 'desc'
? valueB.localeCompare(valueA)
: valueA.localeCompare(valueB);
}
// Handle number comparison
return sortDirection === 'desc'
? Number(valueB) - Number(valueA)
: Number(valueA) - Number(valueB);
});
}// Use the URL to store search state
import { useRouter } from 'next/router';
// Inside your component:
const router = useRouter();
const { q, category, page } = router.query;
// Initialize state from URL
const [searchQuery, setSearchQuery] = useState(q || '');
const [selectedCategory, setSelectedCategory] = useState(category || null);
// Update URL when search changes
useEffect(() => {
router.push({
pathname: router.pathname,
query: {
q: searchQuery || undefined,
category: selectedCategory || undefined,
page: currentPage > 1 ? currentPage : undefined
}
}, undefined, { shallow: true });
}, [searchQuery, selectedCategory, currentPage]);// Create an async version for API data
function useAsyncSearch<T>({
fetchFn,
query,
filters,
page = 1,
itemsPerPage = 10
}) {
const [loading, setLoading] = useState(false);
const [results, setResults] = useState<T[]>([]);
const [total, setTotal] = useState(0);
useEffect(() => {
let isMounted = true;
setLoading(true);
fetchFn({ query, filters, page, itemsPerPage })
.then(response => {
if (isMounted) {
setResults(response.data);
setTotal(response.total);
setLoading(false);
}
})
.catch(error => {
console.error('Search error:', error);
if (isMounted) setLoading(false);
});
return () => { isMounted = false; };
}, [query, filters, page, itemsPerPage]);
return {
results,
total,
loading,
currentPage: page,
totalPages: Math.ceil(total / itemsPerPage)
};
}The useSearch hook transforms complex search functionality into a simple, reusable package. By building it once and using it across your projects, you'll:
Try implementing this hook in your next project, and you'll wonder how you ever built search features without it!