Skip to main content

useFacets

useFacets is the hook for multi-tab, faceted search UIs. It manages URL-bound state for free-text search, facet switching, filters, and pagination.

When to use useFacets

Use useFacets when building a global search page with multiple content type groups and tab or facet switching (e.g. "All / News / Products").

For a dedicated listing or category page — even one with text search — use useListing instead.

Prop API

All props returned by useFacets<T>({ mappers }):

Results

PropTypeDescription
resultsT[]Mapped result items for the current facet and page
isLoadingbooleantrue while results are being fetched
pageIsLoadingbooleantrue while a specific page is loading (separate from facet-wide loading)
featuredT[]Featured results (only populated when featuredResults is configured)
totalCountnumberTotal number of matching results
pageSizenumberCurrent results per page
currentPageIndexnumberZero-based current page index
pagingPagingFull paging state: pageCount, pageIndex, pageSize, totalCount, pagesLoaded
resultsInfoobject{ resultsText, noResultsText, ...custom } from searchResultsInformationMapper
PropTypeDescription
currentSearchTermstringThe active free-text search term
updateSearchTerm(term: string) => voidUpdate the search term (triggers a new search)

Filters

PropTypeDescription
selectedFilters{ [key: string]: string[] }Selected item keys per filter group — each value is a string[]
facetFiltersSearchFiltersFilter definitions from config (use to render filter UI)
updateSelectedFilters(filterGroupKey: string, itemKey: string) => voidToggle a filter — group key first, item value second
clearFilters(clear?: { term?: boolean; keys?: boolean \| string[] }) => voidClear all filters, or just the term, or specific filter keys
PropTypeDescription
updatePageIndex(index: number) => voidNavigate to a page
updatePageSize(size: number) => voidChange results per page
updateSortOrder(orderBy: string[], facet?: string) => voidChange sort expression
updateCurrentFacet(facet: string) => voidSwitch to a different facet
updateCurrentTab(id: number) => voidSwitch to a different tab
sortOrderstring[]Current sort order expressions
currentFacetstringKey of the active facet
currentTabIndexnumberIndex of the active tab
searchTotalCountnumberSum of totalCount across all facets
facetTitles{ key: string; title: string; isSelected: boolean; totalCount: number }[]Facet labels for the current tab

Complete template skeleton

src/app/templates/search/search.template.tsx
import { useFacets } from '@zengenti/contensis-react-base/search';
import SearchTransformations from '~/search/search.transformations';
import type { SearchResultProps } from '~/search/searchResults.mapper';

const SearchPage = () => {
const {
results,
isLoading,
resultsInfo: { resultsText, noResultsText },
totalCount,
pageSize,
currentPageIndex,
currentSearchTerm,
updateSearchTerm,
selectedFilters,
facetFilters,
updateSelectedFilters,
clearFilters,
updatePageIndex,
} = useFacets<SearchResultProps>({ mappers: SearchTransformations });

return (
<div>
{/* Free-text search input */}
<input
type="search"
defaultValue={currentSearchTerm}
onKeyDown={e =>
e.key === 'Enter' &&
updateSearchTerm((e.target as HTMLInputElement).value)
}
/>

{/* Filter UI — iterate facetFilters from config */}
{facetFilters &&
Object.entries(facetFilters).map(([key, filter]) => (
<fieldset key={key}>
<legend>{filter.title}</legend>
{filter.items.map(item => (
<label key={item.key}>
<input
type={filter.isSingleSelect ? 'radio' : 'checkbox'}
checked={selectedFilters[key]?.includes(item.key) ?? false}
onChange={() => updateSelectedFilters(key, item.key)}
{/* group key first, item value second ↑ */}
/>
{item.title}
</label>
))}
</fieldset>
))}

<button onClick={() => clearFilters()}>Clear all filters</button>

{/* Loading state */}
{isLoading && <p>Loading...</p>}

{/* Empty state */}
{!isLoading && noResultsText && <p>{noResultsText}</p>}

{/* Results */}
{!isLoading && results.length > 0 && (
<>
<p>{resultsText}</p>

<ul>
{results.map(item => (
<li key={item.id}>
<a href={item.uri}>{item.title}</a>
</li>
))}
</ul>

{/* Pagination */}
<button
disabled={currentPageIndex === 0}
onClick={() => updatePageIndex(currentPageIndex - 1)}
>
Previous
</button>
<button
disabled={(currentPageIndex + 1) * pageSize >= totalCount}
onClick={() => updatePageIndex(currentPageIndex + 1)}
>
Next
</button>
</>
)}
</div>
);
};

export default SearchPage;

Route wiring

src/app/routes/staticRoutes.ts
import { facets } from '~/schema/search.schema';

{
path: '/search/:facet?',
component: SearchPage,
searchOptions: {
facet: facets.all, // always use schema constant — never a string literal
},
},

Registering the template

src/app/templates/index.ts
import loadable from '@loadable/component';

export const SearchPage = loadable(
() => import('~/templates/search/search.template')
);
caution

updateSelectedFilters argument order: the filter group key comes first, the item value comes second. Reversing these causes a runtime crash: Cannot read properties of undefined (reading 'isSingleSelect').

selectedFilters type: values are string[] — use Array.includes() to check selection state, not .some(f => f.key === x).