How to accommodate high variant product catalogs on a headless Shopify backend?

2021-11-10

greg-rosenke-hWdzH8YY8kk-unsplash

How to accommodate high variant product catalogs on a headless Shopify backend?

A Versatile Product Display Page

Recently, I had the pleasure of helping to develop an eCommerce site. One of my favorite parts was engineering the product display page. I want to walk through some of the logic that was housed in the hook that accompanies the front end component for the PDP (which is how I will refer to the product display page going forward). To lay the groundwork, I introduce the following shape of the collection prop:

Shopify Data Structure
Collection (type of clothing: t-shirt, long-sleeve, etc)
  - Product (color of clothing: red t-shirt, blue t-shirt, etc)
      - Product Variant (size of clothing)

The PDP accepts two props: a collection and a design. The design is an image that the Contentful CDN delivers and is imposed over the clothing as an automated process in the front end. The collection comes from Shopify and is a group of colored options for a clothing type. Additionally, and even more complext, is each color variant has several product sizes. Both the design and collection come in from their respective API's as JSON.

const ProductDisplay: FC<ProductDisplayProps> = ({ collection, design }) => {
  return (
    ...
  )
}

When you first route to the page, the stage needs to be set. That means setting a color variant for the collection and an available size for that color variant.

const [product, setProduct] = useState(viewFirstProductInCollection(collection));
const [productImages, setProductImages] = useState(viewProductImages(product));
const [imageInView, setImageInView] = useState(productImages?.[0]);
const [variant, setVariant] = useState(viewFirstVariantOnProduct(product));
const [selectedColor, setSelectedColor] = useState(collection?.colorOptions?.                  [0]?.productId);

I am a huge fan of optional chaining ?. and the nullish coalescing operator ??

Now that the stage is set, we need to set up the gears that turn when a user selects a different color. This case calls for the useEffect() hook. Whenever a user changes the color variant we need to move a few things around for them.

useEffect(() => {
    setVariant(viewFirstVariantOnProduct(product));
    setProductImages(viewProductImages(product));
    setImageInView(viewProductImages(product)?.[0]);
    isVariantInStock = checkIfVariantIsInStock(variant);
 }, [product]);

We don't have direct access to the product from our color selection component. Instead we need to set up a second useEffect() that changes our product based off of the users selection from the array of color options presented. This looks something like:

 useEffect(() => {
    const selectedProduct = viewProducts(collection)?.edges?.find?.(
      (data) => viewNode(data)?.id === selectedColor
    );
    setProduct(selectedProduct?.node);
    setProductImages(viewProductImages(selectedProduct?.node ?? null));
    const selectedProductImages = viewProductImages(
      selectedProduct?.node ?? null
    );
    setImageInView(selectedProductImages?.[0]);
 }, [selectedColor]);

You may have noticed we are using GraphQL from this code block, the edges? on line 2 is a dead give away

A second hemisphere of this project exists. A page that has an identical layout to the PDP, but will require an array of collections and an array of designs. I had an idea to leverage the same PDP component already built, though adding two more props and make all four of the props optional. This works because of the routing design for the site.

const ProductDisplay: FC<ProductDisplayProps> = ({ collection, design, arrayOfCollections, arrayOfDesigns }) => {
  return (
    ...
  )
}

Routing to the PDP has two possible sets of props that the app will leverage; two arrays of objects, or two objects. We are able to leverage this pair of useState() hooks that allow us to set the stage independent of receiving two objects or two arrays of objects:

const [collectionInView, setCollectionInView] = useState(
    collection ?? customUnitCollections?.[0]
);
const [designInView, setDesignInView] = useState(
    design ?? customUnitDesigns?.[0]
);

Now all the times that we reference collection or design in our code needs to be changed to collectionInView and designInView respectively. Changing a design is done by conveniently firing the setDesignInView(design) hook. Though we need one more useEffect() to trigger all the gears turning when a user selects a different collection from the UI:

useEffect(() => {
    setProduct(viewFirstProductInCollection(collectionInView));
}, [collectionInView]);

Calling setCollectionInView(collection) now cascades everything into place nicely. Instead of rewriting all the code that this page worked off of, we introduced a layer of state above it that changes everything else as a side effect. This entire process resulted in a versatile product display page.