Navigating between pages

This is for:

Developer

When building your commerce interfaces, it’s crucial to manage the state of the current URL using Headless. As users navigate to different pages, you need to ensure that the URL is set and updated correctly to reflect the current view.

Additionally, you may want to manage URL parameters to synchronize them with the state of the interface. For example, when a user filters products on a search or product listing page (PLP), you might want to update the URL to reflect the applied filters. This way, reloading the page or sharing the URL with others will display the same filtered results.

Manage page URL via the context.view object

Use Headless to send the URL information with every Commerce API request, ensuring that actions affecting the view—such as navigating to a new page—emit the correct analytics events.

This is also important for PLPs because you need to specify the URL that corresponds to the PLP configuration you want to target.

Setting URL on engine initialization

When initializing the commerce engine, you can set the URL by using the context.view object.

import { buildCommerceEngine } from '@coveo/headless/commerce';
import { loadCartItemsFromLocalStorage } from '../utils/cart-utils';

export const getEngine = () => {
  if (_engine !== null) {
    return _engine;
  }

  _engine = buildCommerceEngine({
    configuration: {
      organizationId: '<ORGANIZATION_ID>',
      accessToken: '<ACCESS_TOKEN>',
      analytics: {
        trackingId: '<TRACKING_ID>'
      },
      context: {
        currency: '<CURRENCY>',
        country: '<COUNTRY>',
        language: '<LANGUAGE>',
        view: { 1
          url: '<URL>'
        },
      }
      cart: {
        items: loadCartItemsFromLocalStorage() ?? [],
      },
    },
  });

  return _engine;
};
1 Set the view URL to the current page URL.

Modifying the URL on page change

When the user navigates to a new page, you must update the view URL using the Context controller.

import { engine } from './Engine';
import { buildContext } from '@coveo/headless/commerce';

const context = buildContext(engine); 1

onPageChange(newUrl: string) { 2
  context.setView({ url: newUrl });
}
1 Initialize the Context controller by passing in the previously initialized engine.
2 Call the setView method on the Context controller to change the url when the user navigates to a different page.

For more details on how to set the URL in a sample project, see the sample in the Headless repository

Synchronizing parameters with the URL

Additionally, you might need to synchronize search parameters with the URL. This ensures that filters, sorting, and other side effects, such as the current pagination, are reflected in the URL. As a result, users can reload the page or share it with others to see the same filtered results.

Headless for Commerce provides two sub-controllers to manage URL parameters:

  • urlManager: Automatically serializes parameters (query, sort criteria, facet values etc) into a URL-ready string. This controller handles serialization and deserialization internally, making it easy to use and requiring minimal configuration.

    Example

    Your search page is configured with the query set to "hello" and results sorted by descending date. The urlManager serializes this state into the following string: q=hello&sortCriteria=date%20descending.

  • parameterManager: Provides search parameters as an object rather than a string, offering full control over URL serialization and deserialization. This controller is useful when you need to handle URL parameters in a specific way, but it requires a more complex setup.

Note

This article explains how to use the urlManager sub-controller to manage URL parameters on a product listing page (PLP). Similar logic applies when using the parameterManager sub-controller.

The primary difference is that, with the parameterManager sub-controller, you must manually serialize and deserialize parameters. For details, see the generic Headless example.

When using the commerce engine, replace the buildSearchParameterManager with the corresponding parameterManager sub-controller from either the Search controller or the ProductListing controller.

Using the urlManager sub-controller

import {
  buildProductListing,
  Cart,
  CommerceEngine,
  Context,
} from '@coveo/headless/commerce';
import { useEffect, useCallback } from 'react';
import SearchAndListingInterface from '../components/use-cases/search-and-listing-interface/search-and-listing-interface.js';

interface IProductListingPageProps {
  engine: CommerceEngine;
  contextController: Context;
  url: string;
}

export default function ProductListingPage(props: IProductListingPageProps) {
  const {engine, contextController, url} = props;

  const productListingController = buildProductListing(engine);

  const bindUrlManager = useCallback(() => { 1
    const fragment = () => window.location.hash.slice(1); 2
    const urlManager = productListingController.urlManager({ 3
      initialState: {fragment: fragment()},
    });

    const onHashChange = () => {
      urlManager.synchronize(fragment());
    };

    window.addEventListener('hashchange', onHashChange); 4
    const unsubscribeManager = urlManager.subscribe(() => { 5
      const hash = `#${urlManager.state.fragment}`;

      if (!productListingController.state.responseId) {
        window.history.replaceState(null, document.title, hash);
        return;
      }

      window.history.pushState(null, document.title, hash);
    });

    return () => {
      window.removeEventListener('hashchange', onHashChange);
      unsubscribeManager();
    };
  }, [productListingController]);

  useEffect(() => {
    contextController.setView({url}); 6
    const unsubscribe = bindUrlManager(); 7

    if (
      !productListingController.state.isLoading &&
      !productListingController.state.responseId
    ) {
      productListingController.executeFirstRequest();
    } else if (!productListingController.state.isLoading) {
      productListingController.refresh();
    }

    return unsubscribe;
  }, [contextController, url, productListingController, bindUrlManager]);

  return (
    <div className="ProductListingPage">
      <SearchAndListingInterface searchOrListingController={productListingController}/>
    </div>
  );
}
1 Define a function that synchronizes the URL fragment with the urlManager state. Use React’s useCallback hook to memoize the function between renders.
2 Create a function that extracts the hash fragment from the URL using the window object.
3 Initialize the urlManager sub-controller with the initial state of the fragment.

Depending on the product discovery solution you’re implementing, choose the appropriate urlManager sub-controller: Search or ProductListing.

4 Add an event listener to the hashchange event to update the state of the fragment on the urlManager sub-controller.
5 Similarly, subscribe to the urlManager sub-controller to update the URL fragment on the browser using the History API. Now, the state of the fragment in Headless will be synchronized with the URL fragment in the browser.
6 As discussed previously, when a user navigates to a new page, update the URL by calling the setView method on the Context controller.

Calling setView resets the state of facets, sorting, and pagination. This behavior is necessary because the user may have navigated to a different page where these settings are no longer applicable. However, if the user hasn’t navigated to a new page, such as when loading a page for the first time or reloading the same page, it’s important to restore the parameters encoded in the URL fragment. Thus, setView should be called before calling bindUrlManager().

7 Call the bindUrlManager function to synchronize the URL fragment with the urlManager state.
Note

In the example above, setView is called before bindUrlManager to ensure the user’s intended state isn’t lost when the page is loaded or reloaded. Similar logic applies when you’re using other methods to update the Context controller.