Search with Filters
A complete search and filter system with debouncing, URL synchronization, and server-side filtering.
Overview
This template provides:
- Debounced search input
- Multiple filter types (select, multi-select, date range)
- URL state synchronization
- Filter reset functionality
- TanStack Query integration
- Loading and empty states
Dependencies
npm install @tanstack/react-query nuqs
npx shadcn-ui@latest add input select button badge popover calendar
npm install date-fns # for date formattingCode
import { useState, useEffect } from 'react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Search, X } from 'lucide-react';
import { useDebounce } from '@/hooks/useDebounce';
interface SearchInputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
debounceMs?: number;
}
export function SearchInput({
value,
onChange,
placeholder = 'Search...',
debounceMs = 300,
}: SearchInputProps) {
const [localValue, setLocalValue] = useState(value);
const debouncedValue = useDebounce(localValue, debounceMs);
// Sync external value changes
useEffect(() => {
setLocalValue(value);
}, [value]);
// Emit debounced changes
useEffect(() => {
if (debouncedValue !== value) {
onChange(debouncedValue);
}
}, [debouncedValue, onChange, value]);
return (
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={localValue}
onChange={(e) => setLocalValue(e.target.value)}
placeholder={placeholder}
className="pl-9 pr-9"
/>
{localValue && (
<button
onClick={() => {
setLocalValue('');
onChange('');
}}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
)}
</div>
);
}useDebounce Hook
// hooks/useDebounce.ts
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}Features
URL State Synchronization
Filters are automatically synced to URL query parameters using nuqs:
- Shareable URLs with active filters
- Browser back/forward navigation works
- Page refreshes preserve filters
Filter Types
| Type | Component | Use Case |
|---|---|---|
| Search | Debounced input | Text search |
| Single Select | Select dropdown | Enum values |
| Multi-Select | Checkbox popover | Multiple categories |
| Date Range | Calendar popover | Date filtering |
Performance
- Debounced search prevents excessive API calls
- Query key includes all filters for proper caching
placeholderDatakeeps previous results during loading
Date Range Filter
Add date range filtering:
import { Calendar } from '@/components/ui/calendar';
import { format } from 'date-fns';
import { parseAsIsoDateTime } from 'nuqs';
// In filterParsers
const filterParsers = {
// ... other parsers
startDate: parseAsIsoDateTime,
endDate: parseAsIsoDateTime,
};
// Date Range Picker Component
function DateRangePicker({ startDate, endDate, onChange }) {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">
{startDate && endDate
? `${format(startDate, 'MMM d')} - ${format(endDate, 'MMM d')}`
: 'Select dates'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="range"
selected={{ from: startDate, to: endDate }}
onSelect={(range) => onChange(range?.from, range?.to)}
/>
</PopoverContent>
</Popover>
);
}Related
- Data Table - Table with sorting and pagination
- Infinite List - Infinite scroll pattern