Leveraging facets, sorting, and pagination
Leveraging facets, sorting, and pagination
This is for:
DeveloperFacets, sorting, and pagination are essential components of any commerce interface. These features enhance the user experience by making it easier for users to find what they’re looking for when interacting with lists of products.
This article explains how to implement those features in the context of a server-side rendered (SSR) commerce implementation with Coveo Headless and Next.js.
Note
In the following sections, you’ll notice that the Headless front-end doesn’t specify details about what should be returned by the Commerce API, such as which facets are available, the types of sorting criteria, and how many items are displayed per page. Due to the declarative nature of the Commerce API, this information is determined by the configuration associated with the Commerce API request. |
Defining and accessing target controller hooks
Common functionalities like pagination, facets, and sorting are implemented using target controller hooks. Define and retrieve them as follows:
// lib/commerce-engine-config.ts
import {
definePagination,
defineFacetGenerator,
defineSort,
// ...
} from '@coveo/headless-react/ssr-commerce';
export default {
// ...
controllers: {
pagination: definePagination({options: {pageSize: 9}}),
facetGenerator: defineFacetGenerator(),
sort: defineSort(),
// ...
},
} satisfies CommerceEngineDefinitionOptions;
Define the pagination controller with the target default page size. |
Facets
Facets let users filter search results by specific attributes, making it easier to find relevant items.
Facet generator
The FacetGenerator
controller hook lets you access a list of facets that can be rendered in the UI.
See About the Facet Generator.
There are four different types of facets:
-
Regular
-
NumericalRange
-
Category
-
DateRange
The following code snippet showcases how to render facets using the FacetGenerator
based on their type.
'use client';
import {useFacetGenerator} from '@/lib/commerce-engine';
import CategoryFacet from './category-facet';
import DateFacet from './date-facet';
import NumericFacet from './numeric-facet';
import RegularFacet from './regular-facet';
export default function FacetGenerator() {
const {state, methods} = useFacetGenerator();
return (
<nav className="Facets">
{state.map((facetState) => {
const facetId = facetState.facetId;
switch (facetState.type) {
case 'regular': {
return (
<RegularFacet
key={facetId}
controller={methods?.getFacetController(facetId, 'regular')}
staticState={facetState}
/>
);
}
case 'numericalRange':
return (
<NumericFacet
key={facetId}
controller={methods?.getFacetController(
facetId,
'numericalRange'
)}
staticState={facetState}
/>
);
case 'dateRange':
return (
<DateFacet
key={facetId}
controller={methods?.getFacetController(facetId, 'dateRange')}
staticState={facetState}
/>
);
case 'hierarchical':
return (
<CategoryFacet
key={facetId}
controller={methods?.getFacetController(
facetId,
'hierarchical'
)}
staticState={facetState}
/>
);
default:
return null;
}
})}
</nav>
);
}
Implementation details follow in the next section. | |
Get the current list of facets array returned by the facet generator and render the appropriate component based on the facet type . |
Next, let’s look at how to implement the different types of facet components.
Implementing facet components
As an example, we’ll now look at how to implement the Regular
facet component.
Implementation details for the other types of facets can be found in the
sample project in the Headless repository.
// components/facets/regular-facet.tsx
'use client';
import {
RegularFacet as HeadlessRegularFacet,
RegularFacetState,
RegularFacetValue,
} from '@coveo/headless-react/ssr-commerce';
import {useEffect, useRef, useState} from 'react';
interface IRegularFacetProps {
controller?: HeadlessRegularFacet;
staticState: RegularFacetState;
}
export default function RegularFacet(props: IRegularFacetProps) {
const {controller, staticState} = props;
const [state, setState] = useState(staticState);
useEffect(() => {
controller?.subscribe(() => setState(controller.state));
}, [controller]);
const onClickClearSelectedFacetValues = (): void => {
controller?.deselectAll();
};
const onChangeFacetValue = (facetValue: RegularFacetValue): void => {
controller?.toggleSelect(facetValue);
};
const renderFacetValues = () => {
return (
<div className="FacetValuesControls">
<button
aria-label="Clear selected facet values"
className="FacetClearSelected"
disabled={!controller || state.isLoading || !state.hasActiveValues}
onClick={onClickClearSelectedFacetValues}
type="reset"
>
X
</button>
{state.isLoading && (
<span className="FacetLoading"> Facet is loading...</span>
)}
<ul className="FacetValues">
{state.values.map((value) => (
<li className="FacetValue" key={value.value}>
<input
aria-label={`${value.state === 'idle' ? 'Select' : 'Deselect'} facet value '${value.value}'`}
checked={value.state !== 'idle'}
className="FacetValueCheckbox"
disabled={!controller || state.isLoading}
id={value.value}
onChange={() => onChangeFacetValue(value)}
type="checkbox"
></input>
<label className="FacetValueLabel" htmlFor={value.value}>
<span className="FacetValueName">{value.value}</span>
<span className="FacetValueNumberOfProducts">
{' '}
({value.numberOfResults})
</span>
</label>
</li>
))}
</ul>
<button
aria-label="Show more facet values"
className="FacetShowMore"
disabled={!controller || state.isLoading || !state.canShowMoreValues}
onClick={controller?.showMoreValues}
>
+
</button>
<button
aria-label="Show less facet values"
className="FacetShowLess"
disabled={!controller || state.isLoading || !state.canShowLessValues}
onClick={controller?.showLessValues}
>
-
</button>
</div>
);
};
return (
<fieldset className="RegularFacet">
<legend className="FacetDisplayName">
{state.displayName ?? state.facetId}
</legend>
{renderFacetValues()}
</fieldset>
);
}
Create an interface for the props passed down by the FacetGenerator component, namely the Headless facet controller and its static state. |
|
Create a state variable for your component state. You’ll have to keep it synchronized with the state of the Headless facet controller. | |
Create a useEffect hook to subscribe to changes in the headless facet controller state and update your facet component accordingly. |
|
Create a renderFacetValues function to render the facet values to the UI. |
|
Iterate through each value in the state.values array and render the checkbox, label, and number of results. |
|
Render the checkbox input element and bind the onChangeFacetValue method to the onChange event. |
|
Create buttons for various user functionalities. When a user clicks the buttons, call the corresponding method on the controller to interact with the facet. |
Sorting
Sorting lets users order results based on certain criteria like relevance, popularity, or price.
The following code snippet demonstrates how to implement sorting in your commerce SSR interface.
Metadata keys defined in variant and availability data can be used for filtering with facets, but can’t be used for sorting results. |
'use client';
import {useSort} from '@/lib/commerce-engine';
import {SortBy, SortCriterion} from '@coveo/headless-react/ssr-commerce';
export default function Sort() {
const {state, methods} = useSort();
if (state.availableSorts.length === 0) {
return null;
}
const formatSortFieldLabel = (field: {
name: string;
direction?: string;
displayName?: string;
}) => field?.displayName ?? `${field.name} ${field.direction ?? ''}`.trim();
const getSortLabel = (criterion: SortCriterion) => {
switch (criterion.by) {
case SortBy.Relevance:
return 'Relevance';
case SortBy.Fields:
return criterion.fields.map(formatSortFieldLabel);
}
};
return (
<div className="Sort">
<label htmlFor="sport-select">Sort by: </label>
<select
name="sorts"
id="sorts-select"
value={JSON.stringify(state.appliedSort)}
onChange={(e) => methods?.sortBy(JSON.parse(e.target.value))}
disabled={!methods}
>
{state.availableSorts.map((sort, index) => (
<option
key={index}
value={JSON.stringify(sort)}
onSelect={() => methods?.sortBy(sort)}
>
{getSortLabel(sort)}
</option>
))}
</select>
</div>
);
}
Create a getSortLabel function that returns the label for the sort criterion based on its type.
If the current results are sorted by relevance, return Relevance .
Otherwise, stringify the fields and return the value. |
|
Use a select element to display the available sort options, utilizing the string representation of the applied sort as the value.
When the user selects a new sort criterion, call the sortBy method on the controller to sort the results accordingly. |
|
Iterate through the availableSorts and render an option element for each sort criterion.
When the user selects a sort criterion, call the sortBy method on the controller to sort the results by the selected criterion. |
Pagination
Pagination breaks large sets of results into smaller, manageable pages, improving navigation.
The following code snippet demonstrates how to implement pagination in your commerce SSR interface.
'use client';
import {usePagination} from '@/lib/commerce-engine';
export default function Pagination() {
const {state, methods} = usePagination();
const renderPageRadioButtons = () => {
return Array.from({length: state.totalPages}, (_, i) => {
const page = i + 1;
return (
<label className="SelectPage" key={page}>
<input
type="radio"
name="page"
value={page - 1}
checked={state.page === page - 1}
onChange={() => methods?.selectPage(page - 1)}
disabled={methods === undefined}
/>
{page}
</label>
);
});
};
return (
<div className="Pagination">
<div>
Page {state.page + 1} of {state.totalPages}
</div>
<button
className="PreviousPage"
disabled={methods === undefined || state.page === 0}
onClick={methods?.previousPage}
>
{'<'}
</button>
{renderPageRadioButtons()}
<button
className="NextPage"
disabled={methods === undefined || state.page === state.totalPages - 1}
onClick={methods?.nextPage}
>
{'>'}
</button>
</div>
);
}
Create a renderPageRadioButtons function to render radio buttons for each page.
When the user selects a page, call the selectPage method on the controller to navigate to the selected page. |
|
Display buttons to navigate to the previous and next pages.
When the user clicks the buttons, call the previousPage and nextPage methods on the controller to navigate to the previous and next pages, respectively. |