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:
| Field | Type | Description |
|---|---|---|
ownerId | string | Shopify Customer GID for the owner of the list. Ex. gid://shopify/Customer/0123456789123 |
productIds | Array<string> | IDs of the products in this list. We only store the ID instead of the full GID. ["3219876543210", "9876543210987"] |
version | number | Major version of the showroom-api which created this list |
On the browser, we store two local storage items:
| Key | Parsed Type | Description |
|---|---|---|
favorites | Array<string> | Stringified, JSON list of product IDs that have been favorited. Ex. ["3219876543210", "9876543210987"] |
favorites:sync | boolean | true 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:
- If the user went from anonymous to authenticated, we will merge any products favorited during their anonymous session with their authenticated favorites list.
- 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:
- We synchronize the value of the React state with local storage
- If the user is authenticated, we mutate their backend favorites list document via an API call
- We request the enriched Product Data from Storefront via the
/api/products/$productIdsendpoint to incrementally build upon theenrichedProductMapstate
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.