Skip to main content

Search Hooks

The Ever Works Template provides search-related hooks for debouncing user input, managing paginated queries, and implementing infinite scroll. These hooks handle the timing, state management, and data-fetching concerns that underpin search across both public and admin interfaces.

Architecture Overview

Source Files

FilePurpose
hooks/use-debounced-value.tsGeneric debounce primitive for any value type
hooks/use-debounced-search.tsSearch-specific debounce with loading and clear
hooks/use-client-item-filters.tsCombined filter state for client item lists
hooks/use-filters.tsContext accessor for the public FilterProvider
hooks/use-client-items.tsCRUD operations and query management for client items
hooks/use-paginated-query.tsGeneric paginated infinite query wrapper
hooks/use-infinite-loading.tsClient-side infinite scroll over a static array

Debounce Primitives

useDebounceValue

A generic hook that delays updating a value until the user stops changing it.

function useDebounceValue<T>(value: T, delay?: number): T
ParameterTypeDefaultDescription
valueT--The value to debounce
delaynumber300Delay in milliseconds

Uses setTimeout internally with cleanup on unmount. The debounced value only updates after the specified delay of inactivity.

useDebounceSearch

Wraps useDebounceValue with search-specific state: a loading indicator that is true while the user is still typing, and a clearSearch function.

function useDebounceSearch(props: UseDebounceSearchProps): UseDebounceSearchReturn

Props:

PropTypeDefaultDescription
searchValuestring--Current raw search input
delaynumber300Debounce delay in ms
onSearch(value: string) => void | Promise<void>--Callback invoked with debounced value

Return value:

FieldTypeDescription
debouncedValuestringThe debounced search string
isSearchingbooleantrue while input differs from debounced value
clearSearch() => voidResets internal tracking state

The hook skips the callback when the debounced value has not changed since the last invocation, and clears the searching state when the input is empty.

Client Item Filter State

useClientItemFilters

Manages the complete filter state for a client item list: status, search, pagination, and sorting. All filter changes automatically reset pagination to page 1.

function useClientItemFilters(options?: UseClientItemFiltersOptions): UseClientItemFiltersReturn

Options:

OptionTypeDefaultDescription
defaultStatusClientStatusFilter'all'Initial status filter
defaultSearchstring''Initial search term
defaultPagenumber1Initial page
defaultLimitnumber10Items per page
defaultSortBystring'updated_at'Sort field
defaultSortOrder'asc' | 'desc''desc'Sort direction
searchDebounceMsnumber300Search debounce delay

Key return fields:

FieldTypeDescription
paramsClientItemsListParamsCombined params object ready for API calls
isSearchingbooleanWhether search input is still debouncing
hasActiveFiltersbooleanWhether any non-default filter is active
setStatus(status) => voidSet status filter (resets page)
setSearch(search) => voidSet search term
toggleSortOrder() => voidToggle between asc/desc (resets page)
resetFilters() => voidReset all filters to defaults
nextPage / prevPage() => voidPagination helpers

The params object is memoized with useMemo and changes identity only when its constituent values change, making it safe to use as a React Query key dependency.

Context-Based Filter Access

useFilters

Provides access to the public-facing FilterContext. Throws an error if used outside a FilterProvider.

function useFilters(): FilterContextValue

This hook is a thin wrapper around useContext(FilterContext). The filter state itself is managed by useFilterState inside the provider. See the Filter Hooks page for the full public filter system.

Data Fetching Hooks

useClientItems

Full CRUD hook for client-submitted directory items. Combines a list query, a stats query, and update/delete/restore mutations.

function useClientItems(params?: ClientItemsListParams): UseClientItemsReturn

Query configuration:

SettingValue
Stale time5 minutes
Garbage collection time10 minutes
Retry count3
Query key['client', 'items', 'list', params]

Mutations:

ActionMethodEndpointOn success
Update itemPUT/api/client/items/:idToast + invalidate all client item queries
Delete itemDELETE/api/client/items/:idToast + invalidate
Restore itemPOST/api/client/items/:id/restoreToast + invalidate

Prefetching:

The hook exposes prefetchNextPage(nextPage) which preloads the next page of results into the React Query cache.

usePaginatedQuery

A generic wrapper around TanStack useInfiniteQuery for server-paginated endpoints.

function usePaginatedQuery<T>(options: UsePaginatedQueryOptions)
OptionTypeDefaultDescription
endpointstring--API endpoint path
limitnumber10Items per page
sortstring--Sort field
order'asc' | 'desc'--Sort direction
filtersRecord<string, ...>{}Additional query filters
enabledbooleantrueWhether the query is active

The hook uses fetcherPaginated from the API client and derives getNextPageParam from the response metadata (meta.page and meta.totalPages).

useInfiniteLoading

Client-side infinite scroll for a pre-loaded array of items. Progressively reveals more items without additional API calls.

function useInfiniteLoading<T>(props: UseInfiniteLoadingProps<T>): UseInfiniteLoadingResult<T>
PropTypeDefaultDescription
itemsT[]--Full array of items
initialPagenumber--Starting page number
perPagenumberPER_PAGEItems revealed per page

Returns displayedItems (the visible slice), hasMore, isLoading, and a loadMore function. The hook respects the paginationType from useLayoutTheme and only loads more when set to "infinite".

Usage Patterns

const filters = useClientItemFilters({ defaultLimit: 20 });
const { items, isLoading, totalPages } = useClientItems(filters.params);

// In JSX
<AdminSearchBar
value={filters.search}
onChange={filters.setSearch}
isSearching={filters.isSearching}
/>
<AdminStatusTabs
value={filters.status}
onChange={filters.setStatus}
/>

Public Directory with Infinite Scroll

const { displayedItems, hasMore, loadMore } = useInfiniteLoading({
items: allItems,
initialPage: 1,
perPage: 12,
});

// In JSX
{displayedItems.map(item => <Card key={item.id} item={item} />)}
{hasMore && <button onClick={loadMore}>Load More</button>}

Further Reading