Skip to main content

Filter Hooks

The Ever Works Template provides a layered filter system built on React hooks and context. The public-facing directory uses useFilterState with URL synchronization, while the admin and client dashboards use specialized filter hooks with debounced search and status tracking.

Architecture Overview

Source Files

FilePurpose
components/filters/hooks/use-filter-state.tsCore public filter state management
components/filters/hooks/use-filter-url-sync.tsURL synchronization for filters
components/filters/context/filter-context.tsxReact context provider for filters
components/filters/types.tsShared type definitions
hooks/use-admin-filters.tsAdmin dashboard filter hook
hooks/use-client-item-filters.tsClient dashboard filter hook

Type Definitions

Core Types

type SortOption = 'popularity' | 'name-asc' | 'name-desc' | 'date-desc' | 'date-asc';
type CategoryId = string;
type TagId = string;

interface NearMeCoordinates {
latitude: number;
longitude: number;
radius: number; // km
}

interface LocationFilterState {
nearMe?: NearMeCoordinates;
city?: string;
country?: string;
sortByDistance?: boolean;
}

FilterContextType

The full shape exposed by the filter context:

PropertyTypePurpose
searchTermstringCurrent search query
selectedTagsTagId[]Active tag filters
selectedCategoriesCategoryId[]Active category filters
sortBySortOptionCurrent sort order
isFiltersLoadingbooleanLoading indicator after filter change
locationFilterLocationFilterStateActive location-based filter

useFilterState Hook

The useFilterState hook is the main public filter primitive. It manages all filter dimensions, syncs state to the URL, and provides convenience methods for tag/category manipulation.

Initialization

import { useFilterState } from '@/components/filters/hooks/use-filter-state';

const filterState = useFilterState(
initialTag, // Pre-selected tag (from route)
initialCategory, // Pre-selected category (from route)
initialSortBy // Initial sort option
);

Returned API

const {
// State
searchTerm,
selectedTags,
selectedCategories,
sortBy,
selectedTag, // Single tag for navigation
selectedCategory, // Single category for navigation
isFiltersLoading,
locationFilter,

// Setters (auto-sync to URL)
setSearchTerm,
setSelectedTags,
setSelectedCategories,
setSortBy,

// Tag actions
addSelectedTag,
removeSelectedTag,
toggleSelectedTag,

// Category actions
addSelectedCategory,
removeSelectedCategory,
toggleSelectedCategory,
clearSelectedCategories,

// Location actions
setNearMe,
setLocationRadius,
setLocationCity,
setLocationCountry,
clearLocationFilter,

// Global
clearAllFilters,
} = useFilterState();

Category Toggle Behavior

The toggleSelectedCategory function uses single-selection semantics:

const toggleSelectedCategory = useCallback((categoryId: CategoryId) => {
setSelectedCategories(prev =>
prev.includes(categoryId)
? [] // Clicking active category deselects (show all)
: [categoryId] // Clicking inactive category selects ONLY this one
);
}, [setSelectedCategories]);

Loading Indicator

After any filter change, a 400ms loading state is triggered to give visual feedback:

setIsFiltersLoading(true);
updateURL(filterState);
loadingTimeoutRef.current = setTimeout(() => {
setIsFiltersLoading(false);
}, 400);

Scroll Behavior

When filters change, the page scrolls to the filter results area:

const target = document.querySelector('[data-filter-scroll-target]');
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}

Add data-filter-scroll-target to the element that should receive scroll focus after filtering.

useFilterURLSync Hook

The useFilterURLSync hook manages URL updates without triggering Next.js server navigation.

Interface

interface UseFilterURLSyncOptions {
basePath?: string;
locale?: string;
debounceMs?: number; // Default: 300
}

URL Parameters

ParameterSource
tagsComma-separated tag IDs
categoriesComma-separated category IDs
qSearch query text
near_latNear Me latitude
near_lngNear Me longitude
radiusNear Me radius in km
cityCity name filter
countryCountry name filter

Key Behaviors

  1. Uses history.replaceState instead of router.push to avoid triggering Next.js server re-renders
  2. Debounces updates at 300ms by default to prevent excessive history entries during rapid interaction
  3. Skips URL updates on /categories/[slug] and /tags/[slug] routes where the path already reflects the filter
  4. Only updates when changed -- compares the new URL against the current one before calling replaceState
  5. Supports immediate mode -- pass immediate = true to bypass debouncing (used by clearAllFilters)
const { updateURL } = useFilterURLSync({ basePath: '/', locale: 'en' });

// Debounced update
updateURL({ tags: ['ai', 'ml'], categories: [] });

// Immediate update (clears filters instantly)
updateURL({ tags: [], categories: [] }, true);

Location Filters

Location Filter Actions

ActionEffect
setNearMe(coords)Sets geolocation filter, clears city/country
setNearMe(null)Clears geolocation filter
setLocationRadius(km)Updates radius (requires active Near Me)
setLocationCity(name)Sets city filter, clears Near Me and country
setLocationCountry(name)Sets country filter, clears Near Me and city
clearLocationFilter()Clears all location state

Location filters are mutually exclusive -- setting one type clears the others:

Filter Context

The FilterProvider wraps the filter state in a React context for access from any child component:

import { FilterProvider, useFilters } from '@/components/filters/context/filter-context';

// In layout
<FilterProvider initialTag={tag} initialCategory={category}>
<DirectoryPage />
</FilterProvider>

// In any child component
function TagCloud() {
const { selectedTags, toggleSelectedTag } = useFilters();
// ...
}

Admin Filters Hook

The useAdminFilters hook provides a separate filter system for admin dashboards with minimum search length enforcement and multi-select filters.

Usage

import { useAdminFilters } from '@/hooks/use-admin-filters';

const {
searchTerm,
setSearchTerm,
debouncedSearchTerm,
isSearching,
hasActiveSearch,
clearSearch,
statusFilter,
setStatusFilter,
multiFilters,
setMultiFilter,
activeFilterCount,
clearAllFilters,
} = useAdminFilters<ItemStatus>({
minSearchLength: 2,
debounceDelay: 300,
onFiltersChange: () => setCurrentPage(1),
});

Configuration

OptionDefaultPurpose
minSearchLength2Minimum characters before search triggers
debounceDelay300Debounce delay in milliseconds
initialStatus''Default status filter value
initialMultiFilters{}Default multi-select filter values
onFiltersChange--Callback on any filter change (e.g., reset page)

Client Item Filters Hook

The useClientItemFilters hook provides filter state for the client dashboard with combined API parameters.

Usage

import { useClientItemFilters } from '@/hooks/use-client-item-filters';

const {
search,
setSearch,
debouncedSearch,
isSearching,
status,
setStatus,
sortBy,
setSortBy,
params, // Memoized params for API calls
hasActiveFilters,
resetFilters,
} = useClientItemFilters({
defaultSortBy: 'updated_at',
defaultSortOrder: 'desc',
searchDebounceMs: 300,
});

Combined API Parameters

The params object is memoized and ready for direct use with API queries:

interface ClientItemsListParams {
page: number;
limit: number;
status: string;
search: string; // Debounced search value
sortBy: string;
sortOrder: 'asc' | 'desc';
}

Auto-Reset Behavior

All filter changes automatically reset pagination to page 1:

ChangeReset Timing
Status changeImmediate
Search changeOn debounced value settlement
Sort changeImmediate
Limit changeImmediate

Hook Comparison

FeatureuseFilterStateuseAdminFiltersuseClientItemFilters
URL syncYesNoNo
Debounced searchVia URL syncBuilt-inBuilt-in
Tag/category multi-selectYesVia multiFiltersNo
Location filtersYesNoNo
Status filterNoYesYes
Sort options5 presetsNoCustom
Min search lengthNoConfigurableNo
Page reset on changeNoVia callbackAutomatic
Context providerYesNoNo

Best Practices

  1. Use the FilterProvider context for the public directory rather than prop-drilling filter state through many component layers.
  2. Add data-filter-scroll-target to the results container so filter changes scroll users to the right section.
  3. Prefer toggleSelectedTag/toggleSelectedCategory over manual add/remove for simpler component logic.
  4. Use clearAllFilters with immediate URL update to reset the full filter state in one operation.
  5. Set onFiltersChange in admin hooks to reset pagination when filters change.
  6. Avoid useSearchParams for reading initial state -- the URL sync hook deliberately avoids it to prevent SSR hydration issues.