Displaying products
Displaying products
This is for:
DeveloperRegardless 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 |
|
Listing page |
|
Recommendation interface |
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={
() =>
}
>
Add to cart
</button>
</li>
))}
</ul>
);
}
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:
| 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>
);
}
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;
useEffect(() => {
if (methods && !productViewEventEmitted) {
methods?.view({productId, name, price});
productViewEventEmitted = true;
}
}, []);
return null;
}
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
staticState={staticState}
navigatorContext={navigatorContext.marshal}
>
<ProductViewer productId={params.productId} name={name} price={price} />
<!--- ... --->
</StandaloneProvider>
);
}
export const dynamic = 'force-dynamic';
Wrap your product viewer component in a StandaloneProvider to handle static rendering and hydration.
See Headless commerce usage (SSR): Create providers. |