Skip to main content

Favorites

Overviewโ€‹

A staple of many e-commerce websites, Birdy Grey has a custom implementation of favorites, which allows users to save products to a favorites list. This feature is available to both authenticated and unauthenticated users, though authenticated users gain additional benefits of having their favorites synced to our backend.

Data Architectureโ€‹

Persistence Layerโ€‹

Favorites leverages an external, document-based datastore (Google Firestore) to persist favorites lists for authenticated users. Any interactions with the database are always performed server-side, through a service-to-service call via the showroom-api webservice. The showroom-api webservice is an independently built and deployed microservice that leverages the Firebase SDK for Node.js to interact with the Firestore database.

Originally, we were hoping to pull in a Firestore SDK to query the database directly from Hydrogen. However, their client libraries do not support our non-Node runtime and the effort to maintain functional parity with the SDK with type-safety was deemed too high for a custom build.

Data Formatsโ€‹

A Favorites list document in Firestore consists of the following fields:

FieldTypeDescription
ownerIdstringShopify Customer GID for the owner of the list. Ex. gid://shopify/Customer/0123456789123
productIdsArray<string>IDs of the products in this list. We only store the ID instead of the full GID. ["3219876543210", "9876543210987"]
versionnumberMajor version of the showroom-api which created this list

On the browser, we store two local storage items:

KeyParsed TypeDescription
favoritesArray<string>Stringified, JSON list of product IDs that have been favorited. Ex. ["3219876543210", "9876543210987"]
favorites:syncbooleantrue if the user is logged in and should sync mutations with the backend, false for anonymous users

Technical Architectureโ€‹

Initial Loadโ€‹

In the application Root loader, we perform a check to see if the current user is logged in. If the user is logged in, we then check to see if they have a Favorites list in our backend associated with their Customer GID. If they do not, we create one for them. If they do, we fetch the products from the list and query the favorited product IDs in via the Storefront API to build a map of product data to be used later when we want to construct the product cards for favorited products. This initial load must occur in the Root loader to ensure that it is only performed once per application load, which mirrors the singleton nature of the FavoritesContext which we'll use to manage the state of favorites as the user client-side navigates through the application.

After the server data promise resolves, it is passed to the FavoritesContext as props where it is used to initialize the FavoritesContext state.

Front-End Overviewโ€‹

Favorites is primarily powered by the browser's Local Storage API, which allows for a generous persistence layer which should provide sufficient scalability to support a large list of favorited products. Generally, Favorites are restored from local storage (when available) and synchronized with React state via side effects in the FavoritesContext. During initialization, the FavoritesContext compares any server-provided Favorites data with any local storage sourced Favorites data. The initialization considers the following:

  1. If the user went from anonymous to authenticated, we will merge any products favorited during their anonymous session with their authenticated favorites list.
  2. If the user went from authenticated to anonymous (logged out), we will consider local storage stale, ignore any contents, and then clear the local storage entries.

Handling Mutationsโ€‹

Interactions with Favorites is primarily handled through the FavoriteButton component, which can be rendered with a product card when the productId prop is provided. The expected format of the productId prop should be the full GID, e.g. gid://shopify/Product/0123456789123.

When favorites are added or removed from the FavoritesContext state, we perform the following actions:

  1. We synchronize the value of the React state with local storage
  2. If the user is authenticated, we mutate their backend favorites list document via an API call
  3. We request the enriched Product Data from Storefront via the /api/products/$productIds endpoint to incrementally build upon the enrichedProductMap state

The /favorites routeโ€‹

The Favorites route is the page where a user can see all of their Favorited items displayed in a PLP-like grid format. The data from this page is sourced from the FavoritesContext, and leverages the enrichedProductMap to render the Product Cards appropriately.

We've introduced the ability to re-order favorited products on this page as well, via React Aria Components' GridList. If a user is logged in and has more than one item in their Favorites list, they are able to toggle on the Reorder "mode" where they can shuffle cards around. Upon completion, we issue a mutation to our backend to set the new order of Favorited products.

Sharing Favoritesโ€‹

We've added a new route where logged in users can view a favorites list that can be shared to them. This route is hosted at /favorites/$listId where listId is the Firestore document unique ID. Upon landing on this page, the user will be prompted to log in if they are not already. Once logged in, they can view a read-only copy of the original owner's Favorites' list.

One thing to note about this route is that we leverage the Shopify Admin API to retrieve the owner's first name to display on the page, as we have opted not to store this in Firestore.

Referencesโ€‹