Broadleaf Microservices
  • v1.0.0-latest-prod

Wishlist and Recently Viewed List Walkthrough

In this walkthrough, we’ll be examining how we built the commerce app’s Wishlist and Recently Viewed list functionalities. Primarily, we’ll look at how Item Lists are used for such concepts, what persistence and interactive methods are leveraged for registered users versus guest users, and what API calls are necessary in interacting with these lists.

What are Item Lists?

An ItemList is a basic, stateless list of items used for concepts such as wishlists and lists of recently viewed products, which will not require the full information of a cart (e.g. contextual pricing information, offer/campaign support, fulfillment information, etc). Item lists are intended to be easy to transform into a cart and vice versa to support wishlist-like behavior.

Creating & Persisting Item Lists

As we discuss the steps necessary to create an Item List Context and some API calls used in our implementation of Wishlists and Recently Viewed lists, we should keep in mind that there are two general types of users that can interact with these lists in the commerce app’s viewpoint: anonymous guest users and registered users. These two types of users interact with Wishlists and Recently Viewed lists in particular ways that we can choose to emulate or not.

Creating the Item List Context

The goal of creating an Item List Context is to handle resolution & manipulation of item lists as well as handling the state of the item list consistently across all components that need to access it.

In particular for the commerce app, there are two contexts that we use: the ItemListContext for Wishlists and RecentlyViewedContext for Recently Viewed lists.

To set up a context, we can refer to or copy any existing Contexts in the commerce app, or read this tutorial if you are using React, but the general concept is to create a component that will manage the state for other components to be able to access.

Here’s what that could look like in a React project like the commerce app:

Sample Item List Context
// definition of state
type ItemListState = {
  addToItemList: (itemId: string) => void;
  itemList: Array<string>;
  removeFromItemList: (itemId: string) => void;
};

// default state values
const defaultState: ItemListState = {
  addToItemList: () => {},
  itemList: [],
  removeFromItemList: () => {},
};

// create the item list context with the state
const ItemListContext = createContext<ItemListState>(defaultState);

// hook to be able to use the item list context
export const useItemListContext = (): ItemListState =>
  useContext<ItemListState>(ItemListContext);

// provider that will be used to wrap the child components that will access this context
export const ItemListProvider: FC = ({ children }): JSX.Element => {
  const isServer = typeof window === 'undefined';

  if (isServer) {
    return <>{children}</>;
  }

  return <ItemListProviderInner>{children}</ItemListProviderInner>;
};

// actual context operations for state management
export const ItemListProviderInner: FC = ({ children }): JSX.Element => {
    // custom implementations of the state functions
    const addToItemList = () => {...};
    const removeFromItemList = () => {...};
    const context = useMemo(
    () => ({
      addToItemList,
      itemList,
      removeFromItemList,
    }),
    [addToItemList, itemList, removeFromItemList]
  );

  return (
    <ItemListContext.Provider value={context}>
      {children}
    </ItemListContext.Provider>
  );
};

Creating and Updating an Item List

When creating an item list to be managed by the context, it is important to decide how the list will be persisted. In the case for the commerce app, item lists can be persisted by storing data in the browser cache, and by API calls to endpoints that persist the lists in the database for later resolution. This is clearly demonstrated when we examine how Wishlists and Recently Viewed lists are created.

In the commerce app, creating a wishlist starts by clicking the AddToWishlist button, which out of the box is a small heart icon next to the Add to cart button on the Product Detail Page (PDP).

Add to Wishlist Button
Figure 1. Add to Wishlist button on the PDP

When the button displays as an empty heart / heart outline, clicking it will add the product on the current PDP as an item to the user’s wishlist. Clicking this button will either create a new wishlist, or update an existing wishlist by adding the current item on the PDP to it.

For guest users, adding an item to a list means the item is added to a newly created or existing item list in the browser cache.

The browser cache can be accessed and manipulated via the web storage objects available via Javascript, particularly the localStorage and sessionStorage global variables.

For registered users, adding an item to the wishlist means that an API call is made to either create the wishlist item list in the database via the POST /api/cart-operations/item-list-ops endpoint, or to create the item list item for the existing wishlist via an API call to the POST /api/cart-operations/item-list-ops/<ITEM_LIST_ID>/items endpoint.

Request

POST /api/cart-operations/item-list-ops

With the Commerce SDK
import { ItemListClient } from '@broadleaf/commerce-cart';
import { ClientOptions } from '@broadleaf/commerce-core';

async function createItemList(itemList: ItemList, options?: ClientOptions) {
    const client = new ItemListClient(options);
    return client.createItemList(itemList);
  }
Response Payload
{
  "id": "string",
  "name": "string",
  "items": [
    {
      "id": "string",
      "itemListId": "string",
      "itemSkuRef": {
        "sku": "string",
        "productId": "string",
        "variantId": "string"
      },
      "quantity": 0,
      "attributes": {
        "additionalProp1": {},
        "additionalProp2": {},
        "additionalProp3": {}
      },
      "internalAttributes": {
        "additionalProp1": {},
        "additionalProp2": {},
        "additionalProp3": {}
      },
      "attributeChoices": {
        "additionalProp1": {
          "optionLabel": "string",
          "label": "string",
          "value": "string"
        },
        "additionalProp2": {
          "optionLabel": "string",
          "label": "string",
          "value": "string"
        },
        "additionalProp3": {
          "optionLabel": "string",
          "label": "string",
          "value": "string"
        }
      }
    }
  ],
  "attributes": {
    "additionalProp1": {},
    "additionalProp2": {},
    "additionalProp3": {}
  }
}
Tip
More details can be found in the Item List OpenAPI Documentation.
Request

POST /api/cart-operations/item-list-ops/<ITEM_LIST_ID>/items

With the Commerce SDK
import { ItemListClient } from '@broadleaf/commerce-cart';
import { ClientOptions } from '@broadleaf/commerce-core';

async function createItemListItem(listId: string, listItem: ItemListItem, options?: ClientOptions) {
    const client = new ItemListClient(options);
    return client.createItemListItem(listId, listItem);
  }
Response Payload
{
    "id": "string",
    "itemListId": "string",
    "itemSkuRef": {
    "sku": "string",
    "productId": "string",
    "variantId": "string"
    },
    "quantity": 0,
    "attributes": {
    "additionalProp1": {},
    "additionalProp2": {},
    "additionalProp3": {}
    },
    "internalAttributes": {
    "additionalProp1": {},
    "additionalProp2": {},
    "additionalProp3": {}
    },
    "attributeChoices": {
    "additionalProp1": {
        "optionLabel": "string",
        "label": "string",
        "value": "string"
    },
    "additionalProp2": {
        "optionLabel": "string",
        "label": "string",
        "value": "string"
    },
    "additionalProp3": {
        "optionLabel": "string",
        "label": "string",
        "value": "string"
    }
    }
}
Tip
More details can be found in the Item List Item OpenAPI Documentation.

After creating or updating item lists, we would also want to resolve and refetch the item lists stored within the state of the context in order to ensure that the item list in the context reflects that of the browser cache or database, which we do using the GET /api/cart-operations/item-list-ops endpoint. We also store registered user item list ids in the browser cache for easy access to resolve or refetch the item lists from the database, in which case we can use the GET /api/cart-operations/item-list-ops/<LIST_ID> endpoint.

Request

GET /api/cart-operations/item-list-ops

With the Commerce SDK
import { ItemListClient } from '@broadleaf/commerce-cart';
import { ClientOptions } from '@broadleaf/commerce-core';

import { useGetCustomerAccessToken } from '@app/auth/hooks';

async function getItemListsByName(itemListName: string, options?: ClientOptions) {
    const getCustomerAccessToken = useGetCustomerAccessToken();
    const accessToken = await getCustomerAccessToken();
    const client = new ItemListClient(options);
    return client.listItemLists(`name==${itemListName}`, {accessToken});
  }
Response Payload
{
  "first": true,
  "last": true,
  "numberOfElements": 0,
  "pageable": {
    "offset": 0,
    "forward": true,
    "pageSize": 50,
    "paged": true,
    "unpaged": true,
    "underlyingSize": 0
  },
  "sort": [
    {
      "property": "string",
      "direction": "ASC",
      "ignoreCase": true,
      "nullHandling": "string"
    }
  ],
  "content": [
    {
      "id": "string",
      "name": "string",
      "items": [
        {
          "id": "string",
          "itemListId": "string",
          "itemSkuRef": {
            "sku": "string",
            "productId": "string",
            "variantId": "string"
          },
          "quantity": 0,
          "attributes": {
            "additionalProp1": {},
            "additionalProp2": {},
            "additionalProp3": {}
          },
          "internalAttributes": {
            "additionalProp1": {},
            "additionalProp2": {},
            "additionalProp3": {}
          },
          "attributeChoices": {
            "additionalProp1": {
              "optionLabel": "string",
              "label": "string",
              "value": "string"
            },
            "additionalProp2": {
              "optionLabel": "string",
              "label": "string",
              "value": "string"
            },
            "additionalProp3": {
              "optionLabel": "string",
              "label": "string",
              "value": "string"
            }
          }
        }
      ],
      "attributes": {
        "additionalProp1": {},
        "additionalProp2": {},
        "additionalProp3": {}
      }
    }
  ]
}
Tip
More details can be found in the Item List OpenAPI Documentation.
Request

GET /api/cart-operations/item-list-ops/<LIST_ID>

With the Commerce SDK
import { ItemListClient } from '@broadleaf/commerce-cart';
import { ClientOptions } from '@broadleaf/commerce-core';

import { useGetCustomerAccessToken } from '@app/auth/hooks';

async function getItemListById(cachedId: string, options?: ClientOptions) {
    const getCustomerAccessToken = useGetCustomerAccessToken();
    const accessToken = await getCustomerAccessToken();
    const client = new ItemListClient(options);
    return client.getItemList(cachedId, {accessToken});
  }
Response Payload
{
  "id": "string",
  "name": "string",
  "items": [
    {
      "id": "string",
      "itemListId": "string",
      "itemSkuRef": {
        "sku": "string",
        "productId": "string",
        "variantId": "string"
      },
      "quantity": 0,
      "attributes": {
        "additionalProp1": {},
        "additionalProp2": {},
        "additionalProp3": {}
      },
      "internalAttributes": {
        "additionalProp1": {},
        "additionalProp2": {},
        "additionalProp3": {}
      },
      "attributeChoices": {
        "additionalProp1": {
          "optionLabel": "string",
          "label": "string",
          "value": "string"
        },
        "additionalProp2": {
          "optionLabel": "string",
          "label": "string",
          "value": "string"
        },
        "additionalProp3": {
          "optionLabel": "string",
          "label": "string",
          "value": "string"
        }
      }
    }
  ],
  "attributes": {
    "additionalProp1": {},
    "additionalProp2": {},
    "additionalProp3": {}
  }
}
Tip
More details can be found in the Item List OpenAPI Documentation.

As for Recently Viewed lists, in both cases for guest users and registered users, a list of recently viewed items is created in the browser cache when a user navigates to any PDP page for the first time and is subsequently updated for every visit to another PDP. The RecentlyViewed component itself that shows the list of recently viewed items is displayed in the bottom portion of the PDP.

Recently Viewed List
Figure 2. Recently Viewed List

The difference between how the commerce app uses item lists for Wishlists and Recently Viewed lists is that for Wishlists, registered users will have their created/updated lists persisted into the database every time an addition or removal is made, while for Recently Viewed lists, registered users will have their lists persisted into the database only when the lists have hit the configured persistence threshold.

Note

For the commerce app, the number of items that can be displayed at a time in the recently viewed items list is determined either by the .env file property NEXT_PUBLIC_RECENTLY_VIEWED_LIST_DISPLAY_THRESHOLD or the default value in the RecentlyViewedContext. Additionally, the number of items that can be persisted into the database is determined either by the .env file property NEXT_PUBLIC_RECENTLY_VIEWED_LIST_PERSIST_THRESHOLD or the default value in the RecentlyViewedContext.

The idea for these threshold configurations is to not only control the aesthetics of the Recently Viewed list to avoid running the risk of the PDP looking too crowded, but also to limit how much of the browser storage is being taken up by the Recently Viewed list items. The persistence threshold can handle being quite high if, like in the commerce app, only item ids are being stored in the cached list.

Additionally, the registered user’s recently viewed list is instantiated upon login and persisted upon logout, wherein the recently viewed item list cache is cleared for the guest session. Since it is possible for a registered user to have previously saved recently viewed lists, an API call to the GET /api/cart-operations/item-list-ops/<LIST_ID>/items endpoint will retrieve all items of the given item list, which should ideally be the latest item list.

Request

GET /api/cart-operations/item-list-ops/<LIST_ID>/items

With the Commerce SDK
import { ItemListClient } from '@broadleaf/commerce-cart';
import { ClientOptions } from '@broadleaf/commerce-core';

import { useGetCustomerAccessToken } from '@app/auth/hooks';

async function listItemListItems(listId: string, options?: ClientOptions) {
    const getCustomerAccessToken = useGetCustomerAccessToken();
    const accessToken = await getCustomerAccessToken();
    const client = new ItemListClient(options);
    return client.getItemList(listId, {accessToken});
  }
Response Payload
{
  "first": true,
  "last": true,
  "numberOfElements": 0,
  "pageable": {
    "offset": 0,
    "forward": true,
    "pageSize": 50,
    "paged": true,
    "unpaged": true,
    "underlyingSize": 0
  },
  "sort": [
    {
      "property": "string",
      "direction": "ASC",
      "ignoreCase": true,
      "nullHandling": "string"
    }
  ],
  "content": {
    "id": "string",
    "itemListId": "string",
    "itemSkuRef": {
      "sku": "string",
      "productId": "string",
      "variantId": "string"
    },
    "quantity": 0,
    "attributes": {
      "additionalProp1": {},
      "additionalProp2": {},
      "additionalProp3": {}
    },
    "internalAttributes": {
      "additionalProp1": {},
      "additionalProp2": {},
      "additionalProp3": {}
    },
    "attributeChoices": {
      "additionalProp1": {
        "optionLabel": "string",
        "label": "string",
        "value": "string"
      },
      "additionalProp2": {
        "optionLabel": "string",
        "label": "string",
        "value": "string"
      },
      "additionalProp3": {
        "optionLabel": "string",
        "label": "string",
        "value": "string"
      }
    }
  }
}
Tip
More details can be found in the Item List Item OpenAPI Documentation.

Removing Items from an Item List

In the PDP for the commerce app, when the heart icon button displays as a filled heart, that means that the product is currently in the user’s wishlist, so clicking it will remove the item from their wishlist.

For guest users, that means removing the item from the cached wishlist and refetching the guest wishlist item list from the browser cache.

Tip
Refer to MDN documentation for localStorage and sessionStorage web storage global variables to know how to manipulate and fetch from the browser cache.

For registered users, that means removing the item from the wishlist in the browser cacha and in the database via an API call to the DELETE /api/cart-operations/item-list-ops/<LIST_ID>/items/<ITEM_ID> endpoint. Once the wishlist item has been removed via the SDK, the wishlist item list is refetched to be stored in the context.

Request

DELETE /api/cart-operations/item-list-ops/<LIST_ID>/items/<ITEM_ID>

With the Commerce SDK
import { ItemListClient } from '@broadleaf/commerce-cart';
import { ClientOptions } from '@broadleaf/commerce-core';

import { useGetCustomerAccessToken } from '@app/auth/hooks';

async function deleteItemListItem(itemListId: string, itemListItemId: string, options?: ClientOptions) {
    const getCustomerAccessToken = useGetCustomerAccessToken();
    const accessToken = await getCustomerAccessToken();
    const client = new ItemListClient(options);
    client.deleteItemListItem(itemListId, itemListItemId, {accessToken});
  }
Tip
More details can be found in the Item List Item OpenAPI Documentation.

It is easy enough to implement the removeItemListItem function for whatever item list implementation, but in particular for the commerce app, the only instances wherein the Recently Viewed List would be cleared is if the browser’s cache is cleared, either manually or via configuration by closing the browser or session time out — even so, registered users have their recently viewed items persisted into the database when the amount of items viewed matches or exceeds the configured persistence threshold, or when they log out. As long as the cache is not cleared, the recently viewed item list ID stored in the cache can be used to retrieve and repopulate the recently viewed item list to be displayed for registered users, and there is no visible option to delete any items from the recently viewed list for both guest and registered users.

Interacting with Item Lists

Because item lists are designed to be cart-like, item list items are also designed to be similar to cart items, in that they can be used to add to the cart or used as references to the product itself.

Both guest and registered users can navigate to the Wishlist page by hovering the cursor over the Profile User icon on the top right of the site and clicking on the Wishlist menu item.

Profile Wishlist Menu Item
Figure 3. Profile Wishlist Menu Item
Wishlist for Guest
Figure 4. Wishlist page for Guest Users
Wishlist for Registered User
Figure 5. Wishlist page for Registered Users

Once a wishlist has been created and populated, there are multiple ways for the user to interact with it:

  • Add the wishlisted item to the cart

    • Items in the wishlist can be added to the user’s cart by clicking the Add to cart button next to the item, which is the same component found in the PDP.

Tip
Refer to the Add-to-Cart Request Tutorial to learn more about how the Add to cart button works.
  • Open the PDP of the wishlisted item

    • Like other product tiles shown on the site such as on the homepage, clicking the wishlist item’s photo or name navigates to the item’s PDP, since both the item image and name are rendered as buttons that use the product URI for the URL.

  • Remove the item from the wishlist

    • Items in the wishlist can be removed from the wishlist by clicking the Remove link located below the item’s name. The link calls the delete item list items API as earlier mentioned when discussing how item list items can be removed.

For Recently Viewed lists, aside from viewing the list itself on the PDP, the only meaningful interaction the user can have is to open the PDPs of the displayed items by clicking on any of the product tiles. These are the same tiles displayed in pages such as the homepage that when clicked will navigate to the chosen PDP.

Product Tiles for Recently Viewed list and Homepage
Figure 6. Product Tiles for Recently Viewed list and Homepage