Leveraging facets, sorting, and pagination

This is for:

Developer

Facets, 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}}), 1
    facetGenerator: defineFacetGenerator(),
    sort: defineSort(),
    // ...
  },
} satisfies CommerceEngineDefinitionOptions;
1 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'; 1
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) => { 2
        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>
  );
}
1 Implementation details follow in the next section.
2 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 { 1
  controller?: HeadlessRegularFacet;
  staticState: RegularFacetState;
}

export default function RegularFacet(props: IRegularFacetProps) {
  const {controller, staticState} = props;

  const [state, setState] = useState(staticState); 2

  useEffect(() => {
    controller?.subscribe(() => setState(controller.state));
  }, [controller]); 3

  const onClickClearSelectedFacetValues = (): void => {
    controller?.deselectAll();
  };

  const onChangeFacetValue = (facetValue: RegularFacetValue): void => {
    controller?.toggleSelect(facetValue);
  };

  const renderFacetValues = () => { 4
    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) => ( 5
            <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)} 6
                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 7
          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>
  );
}
1 Create an interface for the props passed down by the FacetGenerator component, namely the Headless facet controller and its static state.
2 Create a state variable for your component state. You’ll have to keep it synchronized with the state of the Headless facet controller.
3 Create a useEffect hook to subscribe to changes in the headless facet controller state and update your facet component accordingly.
4 Create a renderFacetValues function to render the facet values to the UI.
5 Iterate through each value in the state.values array and render the checkbox, label, and number of results.
6 Render the checkbox input element and bind the onChangeFacetValue method to the onChange event.
7 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.

Important

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) => { 1
    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 2
        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) => ( 3
          <option
            key={index}
            value={JSON.stringify(sort)}
            onSelect={() => methods?.sortBy(sort)}
          >
            {getSortLabel(sort)}
          </option>
        ))}
      </select>
    </div>
  );
}
1 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.
2 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.
3 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 = () => { 1
    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 2
        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>
  );
}
1 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.
2 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.