Displaying products

This is for:

Developer

Regardless of the product discovery solution you’re building (search, listings, or recommendations), displaying products to the user is an essential part of the commerce experience.

Products can be displayed either as part of a list of products or as a single product on a product detail page (PDP).

This article explains how to display products and ensure that the correct events, such as clicks and product views, are logged when a user interacts with a product.

Displaying lists of products

In your commerce storefront, you’ll often need to display a list of products, for instance when showing search results, product recommendations, or product listing pages. It’s important to log click events when a user interacts with a product in these lists.

Accessing products through controller hooks

Controller hooks let you interact with products and display them in your storefront.

Define target controller hooks

Define and use controller hooks specific to the product discovery solution you’re implementing.

Context Function to use to define the controller hook

Search page

defineProductList

Listing page

defineProductList

Recommendation interface

defineRecommendations

Commerce engine configuration example:

// lib/commerce-engine-config.ts

import {
  defineProductList,
  defineRecommendations,
  // ...
} from '@coveo/headless-react/ssr-commerce';

export default {
  // ...
  controllers: {
    productList: defineProductList(),
    popularViewed: defineRecommendations({
      options: {
        slotId: 'd73afbd2-8521-4ee6-a9b8-31f064721e73',
      },
    }),
    popularBought: defineRecommendations({
      options: {
        slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174',
      },
    }),
    viewedTogether: defineRecommendations({
      options: {
        slotId: 'ff5d8804-d398-4dd5-b68c-6a729c66454b',
      },
    }),
    // ...
  },
  // ...
};

Commerce engine definition example:

// lib/commerce-engine.ts

import {defineCommerceEngine} from '@coveo/headless-react/ssr-commerce';
import engineConfig from './commerce-engine-config';

export const engineDefinition = defineCommerceEngine(engineConfig);

// ...

export const {
  useProductList,
  usePopularBought,
  usePopularViewed,
  useViewedTogether,
  // ...
} = engineDefinition.controllers;

For complete code samples, see commerce-engine-config.ts and commerce-engine.ts.

Access products with target hooks

Regardless of the controller hook you use, you can display products by accessing the state.products object on the controller hook. This object contains a list of products (Product[]) that you can render to the user.

Once you’ve retrieved one of the controller hooks listed above and have access to state.products, you can pass the products to a component that will render them.

The following example ProductList component receives a list of products and renders them using a ProductButtonWithImage component, defined in the following section. It’s meant to work for product listing search pages.

// components/product-list.tsx

'use client';

import {
    useProductList
    // ...
  } from '@/lib/commerce-engine';
import ProductButtonWithImage from './product-button-with-image';
// ...

export default function ProductList() {
  const {state, methods} = useProductList();
  // ...

  return (
    <ul aria-label="Product List">
      {state.products.map((product) => (
        <li key={product.ec_product_id}>
          <ProductButtonWithImage methods={methods} product={product} />

          <button
            onClick={
                () => 1
            }
          >
            Add to cart
          </button>
        </li>
      ))}
    </ul>
  );
}
1 The logic to interact with the cart and log events based on user interaction has been omitted to keep this code simple. For a complete sample, see the Headless project repository.

To display recommendations, you would use the usePopularBought, usePopularViewed, or useViewedTogether controller hooks instead of useProductList.

// components/recommendations/popular-bought.tsx

'use client';

import {usePopularBought} from '@/lib/commerce-engine';
import ProductButtonWithImage from './product-button-with-image';

export default function PopularBought() {
  const {state, methods} = usePopularBought();

  return (
    <>
      <ul>
        <h3>{state.headline}</h3>
        {state.products.map((product) => (
          <li key={product.ec_product_id}>
            <ProductButtonWithImage methods={methods} product={product} />
          </li>
        ))}
      </ul>
    </>
  );
}
Note

Also, to display recommendations, make sure to enable the target recommendation hook when fetching the static state. See Display recommendations with your recommendation provider.

Display the target product

Whether it’s on a search page, a product listing page, a recommendation carousel, or as an instant product displayed below the search box, every product displayed in a product list must use the product interactiveProduct.select function to ensure that the correct analytics event is logged when the product is clicked.

// components/product-button-with-image.tsx

import {
  Product,
  ProductList,
  Recommendations,
} from '@coveo/headless-react/ssr-commerce';
import Image from 'next/image';
import {useRouter} from 'next/navigation';

export interface ProductButtonWithImageProps {
  methods: 1
    | Omit<Recommendations, 'state' | 'subscribe'>
    | Omit<ProductList, 'state' | 'subscribe'>
    | undefined;
  product: Product;
}

export default function ProductButtonWithImage({
  methods,
  product,
}: ProductButtonWithImageProps) {
  const router = useRouter();

  const onProductClick = (product: Product) => {
    methods?.interactiveProduct({options: {product}}).select();
    router.push(
      `/products/${product.ec_product_id}?name=${product.ec_name}&price=${product.ec_price}`
    );
  };

  return (
    <button disabled={!methods} onClick={() => onProductClick(product)}>
      {product.ec_name}
      <Image
        src={product.ec_images[0]}
        alt={product.ec_name!}
        width={50}
        height={50}
      />
    </button>
  );
}
1 The methods prop is a union of the methods available through the Recommendations and ProductList controller hooks. This lets the component work for both use cases. The undefined type is for the case of static rendering, before hydration, when the hook isn’t yet available.

Displaying a product on a product detail page

When displaying a product on a product detail page (PDP), define and use a hook on the ProductView controller to ensure that the correct product view event is logged when a user views a product.

Create a product viewer component that you’ll then include in your product detail pages.

// components/product-viewer.tsx

'use client';

import {useProductView} from '@/lib/commerce-engine';
import {useEffect} from 'react';

interface Product {
  productId: string;
  name: string;
  price: number;
}

export default function ProductViewer({productId, name, price}: Product) {
  const {methods} = useProductView();
  const productViewEventEmitted = false; 1

  useEffect(() => {
    if (methods && !productViewEventEmitted) {
      methods?.view({productId, name, price});
      productViewEventEmitted = true;
    }
  }, []);

  return null;
}
1 A flag so the product view event is logged only once.

Then, include the ProductViewer component in your product detail page component.

// app/products/[productId].page.tsx

// ...
import ProductViewer from '@/components/product-viewer';

export default async function ProductDescriptionPage({
  params,
  searchParams,
}: {
  params: {productId: string};
  searchParams: Promise<{[key: string]: string | string[] | undefined}>;
}) {

  // ...

  const resolvedSearchParams = await searchParams;
  const price = Number(resolvedSearchParams.price) ?? NaN;
  const name = Array.isArray(resolvedSearchParams.name)
    ? params.productId
    : (resolvedSearchParams.name ?? params.productId);

  return (
    <StandaloneProvider 1
      staticState={staticState}
      navigatorContext={navigatorContext.marshal}
    >
      <ProductViewer productId={params.productId} name={name} price={price} />
      <!--- ... --->
    </StandaloneProvider>
  );
}

export const dynamic = 'force-dynamic';
1 Wrap your product viewer component in a StandaloneProvider to handle static rendering and hydration. See Headless commerce usage (SSR): Create providers.