Managing the cart

This is for:

Developer

When building your Coveo-powered commerce interfaces using server-side rendering (SSR), it’s crucial to manage the Headless cart state and keep it synchronized with your own cart management solution.

Use Headless to send the cart state with every Commerce API request, ensuring actions affecting the cart—​such as adding or removing a product, or purchasing the cart’s contents—​emit the correct cart and purchase events.

Managing the cart with Headless involves the following steps:

Note

Headless doesn’t replace your own cart management solution. For example, it doesn’t replace your mechanisms for storing the cart across user sessions or devices. The goal of this article is to help you keep the Headless cart state synchronized with your own cart management solution.

Set the cart state when fetching the static state

When you fetch the static state in your pages, pass the cart’s initial state by setting the controllers.cart object in the target engine configurations.

// app/(listing)/[category]/page.tsx

import * as externalCartAPI from '@/actions/external-cart-api';
// ...

export default async function Listing({
  params,
  searchParams,
}: {
  params: {category: string};
  searchParams: Promise<URLSearchParams>;
}) {

  const items = await externalCartAPI.getCart(); 1

  const staticState = await listingEngineDefinition.fetchStaticState({
    controllers: {
      cart: {initialState: {items}}, 2
      // ...
    },
  });

  const recsStaticState = await recommendationEngineDefinition.fetchStaticState(
    {
      controllers: {
        cart: {initialState: {items}}, 3
        // ...
      },
    }
  );
  // ...
}
1 Use your own logic to retrieve the cart items and format them for Headless. In this example, we use a fictitious externalCartAPI module defined in the next section.
2 Pass in the cart’s initial state by specifying the CartInitialState object.
3 If you use separate engines for different parts of your page, pass the initial cart items to each engine.

Modify the cart state on user interaction

Whenever the user changes the cart’s contents, you must both:

  • Update the cart state using the Cart controller.

  • Save the cart state so it can be restored upon initializing the commerce engine.

Changing the cart’s contents can include actions such as adding or removing a product, adjusting the quantity of a product, or purchasing the cart’s contents.

The following example file contains a number of utility functions that illustrate how to modify the cart state when the user interacts with the cart, using external-cart-api defined in the next subsection.

// utils/cart.ts

import * as externalCartAPI from '@/actions/external-cart-api';
import {
  Cart as HeadlessCart,
  CartItem as HeadlessCartItem,
  Product as HeadlessProduct,
  CartState as HeadlessCartState,
  InstantProducts,
  ProductList,
} from '@coveo/headless-react/ssr-commerce';

type HeadlessSSRCart = Omit<HeadlessCart, 'state' | 'subscribe'>; 1

export async function adjustQuantity(
  headlessCart: HeadlessSSRCart,
  item: HeadlessCartItem,
  delta: number
) {
  const updatedItem = {
    ...item,
    quantity: item.quantity + delta,
  };

  headlessCart.updateItemQuantity(updatedItem);
  await externalCartAPI.updateItemQuantity(updatedItem); 2
}

export async function addToCart(
  headlessCart: HeadlessSSRCart,
  headlessCartState: HeadlessCartState,
  product: HeadlessProduct,
  methods:
    | Omit<InstantProducts | ProductList, 'state' | 'subscribe'>
    | undefined
) {
  const existingItem = headlessCartState.items.find(
    (item) => item.productId === product.ec_product_id
  );

  const quantity = existingItem ? existingItem.quantity + 1 : 1;
  const item = {
    name: product.ec_name!,
    price: product.ec_price!,
    productId: product.ec_product_id!,
    quantity: quantity,
  };

  headlessCart.updateItemQuantity(item);

  await externalCartAPI.addItemToCart(item);

  methods?.interactiveProduct({options: {product}}).select(); 3
}

export async function purchase(
  headlessCart: HeadlessSSRCart,
  totalPrice: number
) {
  headlessCart.purchase({id: crypto.randomUUID(), revenue: totalPrice}); 4

  await externalCartAPI.clearCart();
}

export async function emptyCart(headlessCart: HeadlessSSRCart) {
  headlessCart.empty();

  await externalCartAPI.clearCart();
}
1 The type of the methods passed by the Cart controller hook.
2 Update the cart.
3 When sending cart events directly from the search result page, product listing pages (PLPs), or recommendation slots, you must send an additional click event along with the cart event. See Capture cart events: Send an additional click event.
4 Generate a unique ID for the purchase event. In a real implementation, you would likely retrieve this ID from an external service.

Then, leverage these utility functions in your components to interact with the cart.

// components/cart.tsx

'use client';

import {useCart, useContext} from '@/lib/commerce-engine';
import {adjustQuantity, emptyCart, purchase} from '@/utils/cart';
// ...

export default function Cart() {
  const cart = useCart();
  const {state: contextState} = useContext();

  const isCartEmpty = () => {
    return cart.state.items.length === 0;
  };

  const language = () => contextState.language;
  const currency = () => contextState.currency;

  return (
    <div>
      <ul id="cart">
        {cart.state.items.map((item, index) => (
          <li key={index}>
            <p>
              <span>Name: </span>
              <span>{item.name}</span>
            </p>
            <!-- ... -->
            <button onClick={() => adjustQuantity(cart.methods!, item, 1)}>
              Add one
            </button>
            <button onClick={() => adjustQuantity(cart.methods!, item, -1)}>
              Remove one
            </button>
            <button
              onClick={() => adjustQuantity(cart.methods!, item, -item.quantity)}
            >
              Remove all
            </button>
          </li>
        ))}
      </ul>
      <!-- ... -->
      <button
        disabled={isCartEmpty()}
        onClick={() => purchase(cart.methods!, cart.state.totalPrice)}
      >
        Purchase
      </button>
      <button disabled={isCartEmpty()} onClick={() => emptyCart(cart.methods!)}>
        Empty cart
      </button>
    </div>
  );
}

This component illustrates one possible design for the cart page in a commerce application. For more details on how this component is used in a sample project, see the example in the Headless repository.

Users can also interact with the cart from other pages, such as a PLP, where they can add products directly to the cart. In these cases, use the appropriate Cart controller hook methods to ensure that analytics events are emitted correctly.

Saving the cart state

In single-page applications (SPAs), the cart state might be lost when the page is refreshed, and in multi-page applications (MPAs), the cart state can be lost when users navigate to different pages. Therefore, it’s important to save the cart state whenever it’s modified so that it can be restored when the commerce engine is initialized.

This isn’t managed by Headless, you must implement it yourself. For example, you could use an external API to save the cart state. You could also use a browser cookie, as in the following example.

// actions/external-cart-api.ts

'use server';

import {CartItem} from '@coveo/headless-react/ssr-commerce';
import {cookies} from 'next/headers';

function getCartFromCookies(): CartItem[] {
  const cartCookie = cookies().get('headless-cart');
  return cartCookie ? JSON.parse(cartCookie.value) : [];
}

function setCartInCookies(cart: CartItem[]) {
  cookies().set('headless-cart', JSON.stringify(cart), {
    path: '/',
    maxAge: 60 * 60 * 24,
  });
}

export async function getCart(): Promise<CartItem[]> {
  return getCartFromCookies();
}

export async function addItemToCart(newItem: CartItem): Promise<CartItem[]> {
  const cart = getCartFromCookies();
  const existingItem = cart.find(
    (item) => item.productId === newItem.productId
  );
  if (existingItem) {
    existingItem.quantity += 1;
  } else {
    cart.push(newItem);
  }
  setCartInCookies(cart);
  return cart;
}

export async function updateItemQuantity(
  updatedItem: CartItem
): Promise<CartItem[]> {
  let cart = getCartFromCookies();
  const existingItem = cart.find(
    (item) => item.productId === updatedItem.productId
  );
  if (existingItem) {
    if (updatedItem.quantity === 0) {
      cart = cart.filter((item) => item.productId !== updatedItem.productId);
    } else {
      existingItem.quantity = updatedItem.quantity;
    }
  }
  setCartInCookies(cart);
  return cart;
}

export async function clearCart(): Promise<CartItem[]> {
  setCartInCookies([]);
  return [];
}