Skip to content

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>
  )
}
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:

OperatorDescriptionExample
equalsExact match{ field: 'status', operator: 'equals', value: 'active' }
containsSubstring match{ field: 'name', operator: 'contains', value: 'john' }
startsWithPrefix match{ field: 'email', operator: 'startsWith', value: 'admin' }
endsWithSuffix match{ field: 'email', operator: 'endsWith', value: '.com' }
inValue in array{ field: 'status', operator: 'in', value: ['active', 'pending'] }
notInValue 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
}

  • useBulkSelection — Often combined with filtering for filtered + selectable lists