Skip to main content

useListing

useListing is the hook for dedicated listing and category pages. It supports both text search and UI filters, and binds its state to the URL.

When to use useListing

Use useListing for any dedicated listing or category page — a page whose primary purpose is showing a filtered, browsable set of results. This includes pages with text search input.

For a global, multi-tab search UI with facet switching, use useFacets instead.

Key differences from useFacets

useFacetsuseListing
Search term propcurrentSearchTermsearchTerm
Filter state propfacetFiltersfilters
Filter initAlways definedValues may be undefined during initialisation — guard before rendering
Use caseGlobal multi-tab searchDedicated listing page

Prop API

PropTypeDescription
resultsT[]Mapped result items
isLoadingbooleantrue while results are being fetched
resultsInfoobject{ resultsText, noResultsText, ...custom }
searchTermstringCurrent free-text search term
updateSearchTerm(term: string) => voidUpdate the search term
filtersFiltersFilter definitions from config — values may be undefined during init
selectedFilters{ [key: string]: string[] }Selected item keys per filter group
updateSelectedFilters(filterGroupKey: string, itemKey: string) => voidToggle a filter — group key first, item value second
clearFilters(opts?) => voidClear filters, optionally also the search term
pagingPaging{ pageIndex, pageSize, totalCount, pageCount }
updatePageIndex(index: number) => voidNavigate to a page

Complete template skeleton

src/app/templates/plantListing/plantListing.template.tsx
import type { Filter } from '@zengenti/contensis-react-base/models/search/models/SearchState';
import { useListing } from '@zengenti/contensis-react-base/search';

import SearchTransformations from '~/search/search.transformations';
import type { SearchResultProps } from '~/search/searchResults.mapper';

const PlantListing = () => {
const {
results,
isLoading,
resultsInfo: { resultsText, noResultsText },
searchTerm, // note: not currentSearchTerm
updateSearchTerm,
selectedFilters,
filters, // note: not facetFilters — values may be undefined during init
updateSelectedFilters,
clearFilters,
paging, // paging.pageIndex, paging.pageSize, paging.totalCount
updatePageIndex,
} = useListing<SearchResultProps>({ mappers: SearchTransformations });

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

{/* Filter UI — cast entries to guard against undefined values during init */}
{filters &&
(Object.entries(filters) as [string, Filter | undefined][]).map(([key, filter]) => {
if (!filter) return null; // values are undefined during initialisation
return (
<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>
);
})}

{isLoading && <p>Loading...</p>}
{!isLoading && noResultsText && <p>{noResultsText}</p>}

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

<p>{resultsText}</p>

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

export default PlantListing;

Route wiring

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

{
path: '/plants',
component: PlantListing,
searchOptions: {
listingType: listings.plants, // note: listingType, not facet; always use schema constant
},
},

Registering the template

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

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

filters values: Unlike useFacets's facetFilters, the filters object from useListing has values typed as Filter | undefined during initialisation. Always guard with if (!filter) return null before rendering.

updateSelectedFilters argument order: group key first, item value second. Reversing these causes a runtime crash.