Implement instant products (CSR)

This is for:

Developer

Use the InstantProducts controller to display relevant products in real time as users type in the search box.

This is step 1 of the instant products and filter suggestions guide. See that page for prerequisites.

The Coveo Headless repository contains a sample implementation.

Implement an instant products component

The following component subscribes to the InstantProducts controller state and renders a list of products that updates as the user types.

// src/components/instant-products/instant-products.tsx
import type {InstantProducts as HeadlessInstantProducts} from '@coveo/headless/commerce';
import {useEffect, useState} from 'react';
import {onClickProduct} from './actions/on-click-product.js';

interface IInstantProductProps {
  controller: HeadlessInstantProducts;
  navigate: (pathName: string) => void;
}

export default function InstantProducts(props: IInstantProductProps) {
  const {controller, navigate} = props;
  const [state, setState] = useState(controller.state);

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

  if (!state.query) {
    return null;
  }

  return (
    <div className="InstantProducts">
      {state.products.length === 0 ? (
        <p className="NoInstantProducts">
          No instant products for query <b>{state.query}</b>
        </p>
      ) : (
        <>
          <p>
            Instant products for query <b>{state.query}</b>
          </p>
          <ul className="InstantProducts">
            {state.products.map((product) => (
              <li className="Product" key={product.permanentid}>
                <button
                  onClick={() => onClickProduct(product, controller, navigate)}
                  type="button"
                >
                  {product.ec_name} ({product.ec_product_id})
                </button>
              </li>
            ))}
          </ul>
        </>
      )}
    </div>
  );
}

Click tracking

When a user clicks a product in the instant products list, log the click event using the InteractiveProduct sub-controller.

The InteractiveProduct sub-controller is available on the InstantProducts controller via the interactiveProduct method. When you call select() on this sub-controller, it automatically:

  • Attaches the correct responseId from the underlying product suggestion response.

  • Includes all required product metadata in the analytics event.

  • Logs a valid click event through the Event Protocol.

This is the same InteractiveProduct pattern used for Search and Product Listing controllers.

Basic usage

Use the interactiveProduct method on the InstantProducts controller, passing the product the user clicked, then call select().

// src/components/instant-products/actions/on-click-product.ts
import type {InstantProducts, Product} from '@coveo/headless/commerce';

export function onClickProduct(
  product: Product,
  controller: InstantProducts,
  navigate: (pathName: string) => void
) {
  controller.interactiveProduct({options: {product}}).select(); 1

  const productId = product.ec_product_id ?? product.permanentid;
  navigate(`/product/${productId}`); 2
}
1 Calling select() on the InteractiveProduct sub-controller logs the click event with all required metadata, including the responseId.
2 After logging the event, navigate to the product detail page.

Handling delayed selections

The InteractiveProduct sub-controller also supports beginDelayedSelect and cancelPendingSelect for hover interactions where you want to track a selection only if the user lingers on a product long enough.

// src/components/instant-products/actions/on-delayed-select-product.ts
import type {InstantProducts, Product} from '@coveo/headless/commerce';

export function getProductHoverHandlers(
  product: Product,
  controller: InstantProducts,
  navigate: (pathName: string) => void
) {
  const interactiveProduct = controller.interactiveProduct({
    options: {product},
  });

  return {
    onMouseEnter: () => interactiveProduct.beginDelayedSelect(), 1
    onMouseLeave: () => interactiveProduct.cancelPendingSelect(), 2
    onClick: () => {
      interactiveProduct.select(); 3
      const productId = product.ec_product_id ?? product.permanentid;
      navigate(`/product/${productId}`);
    },
  };
}
1 When the user hovers over a product, begin a delayed selection. If the user remains on the product long enough, the click event is logged automatically.
2 If the user moves away before the delay elapses, cancel the pending selection so no event is logged.
3 On click, call select() on the same instance to cancel any pending delayed select and immediately log the event.

Common mistake: Manually emitting click events

A common implementation error is to attempt manually emitting an ec.productClick event using the Event Protocol relay instance and the product data from instant products state:

// ❌ WRONG: This does NOT work for Instant Products
const product = instantProductsState.products[0];
relay.emit('ec.productClick', {
  product: {
    productId: product.ec_product_id,
    name: product.ec_name,
    price: product.ec_price,
  },
  responseId: instantProductsState.responseId, // undefined!
});

This fails because instantProductsState.responseId doesn’t exist. The responseId for product suggestions is managed internally by Headless and is only accessible through the InteractiveProduct sub-controller.

Always use the interactiveProduct sub-controller instead:

// ✅ CORRECT
controller.interactiveProduct({options: {product}}).select();

What’s next

After building your instant products component, implement filter suggestions to display facet-based suggestions as users type in the search box.