Appearance
useFilter
What this solves
Filtering and searching are ubiquitous in admin UIs:
- Text search across multiple fields
- Multi-select filters (status, category, tags)
- Combined filter + search workflows
- Filter state management and reset
Ad-hoc implementations often have:
- Inconsistent filter logic across screens
- Duplicated search debouncing and normalization
- Complex state for tracking active filters
useFilter, useTextSearch, and useMultiSelectFilter provide consistent client-side filtering with a predictable API.
When to use it
Use these hooks for client-side filtering when:
- You have a manageable dataset loaded in memory
- You need consistent filter behavior across screens
- You want to combine multiple filter types (text + multi-select)
When NOT to use it
- For server-side filtering with pagination (pass filter params to your API instead)
- For very large datasets where client-side filtering would be slow
- When filters need to be persisted in URL state (combine with your routing solution)
Basic usage
useFilter — Full filtering with multiple criteria
tsx
import { useFilter } from '@/shared/hooks'
type User = { id: string; name: string; email: string; status: 'active' | 'inactive' }
export function UserList({ users }: { users: User[] }) {
const {
filteredItems,
search,
setSearch,
filters,
addFilter,
removeFilter,
clearFilters,
hasActiveFilters,
} = useFilter({
items: users,
searchFields: ['name', 'email'],
})
return (
<div>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search users..."
/>
<button
type="button"
onClick={() => addFilter({ field: 'status', operator: 'equals', value: 'active' })}
>
Show active only
</button>
{hasActiveFilters && (
<button type="button" onClick={clearFilters}>
Clear filters
</button>
)}
<ul>
{filteredItems.map((user) => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
</div>
)
}useTextSearch — Simple text search
tsx
import { useTextSearch } from '@/shared/hooks'
type Product = { id: string; name: string; description: string }
export function ProductSearch({ products }: { products: Product[] }) {
const { filteredItems, search, setSearch } = useTextSearch({
items: products,
searchFields: ['name', 'description'],
})
return (
<div>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search products..."
/>
{filteredItems.map((p) => (
<div key={p.id}>{p.name}</div>
))}
</div>
)
}useMultiSelectFilter — Multi-select filtering
tsx
import { useMultiSelectFilter } from '@/shared/hooks'
type Task = { id: string; title: string; status: 'todo' | 'in_progress' | 'done' }
export function TaskFilter({ tasks }: { tasks: Task[] }) {
const {
filteredItems,
selectedValues,
toggleValue,
clearSelection,
isSelected,
} = useMultiSelectFilter({
items: tasks,
field: 'status',
})
const statuses = ['todo', 'in_progress', 'done'] as const
return (
<div>
{statuses.map((status) => (
<label key={status}>
<input
type="checkbox"
checked={isSelected(status)}
onChange={() => toggleValue(status)}
/>
{status}
</label>
))}
{selectedValues.length > 0 && (
<button type="button" onClick={clearSelection}>
Clear
</button>
)}
<ul>
{filteredItems.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
</div>
)
}Filter operators
The useFilter hook supports these operators:
| Operator | Description | Example |
|---|---|---|
equals | Exact match | { field: 'status', operator: 'equals', value: 'active' } |
contains | Substring match | { field: 'name', operator: 'contains', value: 'john' } |
startsWith | Prefix match | { field: 'email', operator: 'startsWith', value: 'admin' } |
endsWith | Suffix match | { field: 'email', operator: 'endsWith', value: '.com' } |
in | Value in array | { field: 'status', operator: 'in', value: ['active', 'pending'] } |
notIn | Value not in array | { field: 'role', operator: 'notIn', value: ['guest'] } |
API reference (concise)
useFilter
ts
interface FilterCriteria<T> {
field: keyof T
operator: 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'in' | 'notIn'
value: unknown
caseSensitive?: boolean
}
interface UseFilterOptions<T> {
items: T[]
initialFilters?: FilterCriteria<T>[]
initialSearch?: string
searchFields?: (keyof T)[]
customFilter?: (item: T, filters: FilterCriteria<T>[], search: string) => boolean
}
interface UseFilterResult<T> {
filteredItems: T[]
filters: FilterCriteria<T>[]
search: string
hasActiveFilters: boolean
setSearch: (search: string) => void
addFilter: (filter: FilterCriteria<T>) => void
removeFilter: (field: keyof T) => void
updateFilter: (field: keyof T, filter: Partial<FilterCriteria<T>>) => void
clearFilters: () => void
resetAll: () => void
}useTextSearch
ts
interface UseTextSearchOptions<T> {
items: T[]
searchFields: (keyof T)[]
initialSearch?: string
caseSensitive?: boolean
}
interface UseTextSearchResult<T> {
filteredItems: T[]
search: string
setSearch: (search: string) => void
clearSearch: () => void
}useMultiSelectFilter
ts
interface UseMultiSelectFilterOptions<T, V> {
items: T[]
field: keyof T
initialSelected?: V[]
}
interface UseMultiSelectFilterResult<T, V> {
filteredItems: T[]
selectedValues: V[]
toggleValue: (value: V) => void
selectValue: (value: V) => void
deselectValue: (value: V) => void
clearSelection: () => void
isSelected: (value: V) => boolean
}Related primitives
- useBulkSelection — Often combined with filtering for filtered + selectable lists