Managing the cart
Managing the cart
This is for:
DeveloperWhen 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();
const staticState = await listingEngineDefinition.fetchStaticState({
controllers: {
cart: {initialState: {items}},
// ...
},
});
const recsStaticState = await recommendationEngineDefinition.fetchStaticState(
{
controllers: {
cart: {initialState: {items}},
// ...
},
}
);
// ...
}
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. |
|
Pass in the cart’s initial state by specifying the CartInitialState object. |
|
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'>;
export async function adjustQuantity(
headlessCart: HeadlessSSRCart,
item: HeadlessCartItem,
delta: number
) {
const updatedItem = {
...item,
quantity: item.quantity + delta,
};
headlessCart.updateItemQuantity(updatedItem);
await externalCartAPI.updateItemQuantity(updatedItem);
}
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();
}
export async function purchase(
headlessCart: HeadlessSSRCart,
totalPrice: number
) {
headlessCart.purchase({id: crypto.randomUUID(), revenue: totalPrice});
await externalCartAPI.clearCart();
}
export async function emptyCart(headlessCart: HeadlessSSRCart) {
headlessCart.empty();
await externalCartAPI.clearCart();
}
The type of the methods passed by the Cart controller hook. |
|
Update the cart. | |
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. | |
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 [];
}