Product Groupings: Using Pack product groups in your storefront
A product grouping links several Shopify products so your storefront can show them as one product. For example, a shirt sold as a separate Shopify product in each color can show every color as an option on each of the shirts' product pages.
Merchants create groups in Pack's admin under Products > Groups. See Groups. Pack stores which products belong to each group. Your storefront queries the groups and decides what to do with them.
The code on this page comes from the Blueprint.
The functions and hooks below are Blueprint files, not exports of
@pack/hydrogen or @pack/react. If your storefront isn't built on the
Blueprint, copy the files you need.
Group data
The groups query returns the groups set in Pack, 25 at a time. Each group has
a title, a description and its products. Only each product's handle and id
are stored in Pack, so you load the rest of the product data from Shopify.
A group can also have subgroups. Each subgroup has its own title, description and products. On the product page, the Blueprint lists each subgroup's colors as a separate set under the subgroup's title.
app/data/graphql/pack/settings.ts
export const PRODUCT_GROUPINGS_QUERY = `
query ProductGroupings($after: String) {
groups(first: 25, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
title
description
products {
handle
id
}
subgroups {
id
title
description
products {
handle
id
}
}
}
}
}
}
` as const;
A product can belong to only one group, so to find a product's group, look for the group whose products or subgroups include its handle.
Fetch every group
getProductGroupings follows pageInfo until it has every group, and caches
the result with Hydrogen's CacheLong strategy. It returns null if the query
fails.
app/lib/server-utils/pack.server.ts
export const getProductGroupings = async (context: AppLoadContext) => {
try {
const getAllProductGroupings = async ({
groupings,
cursor,
}: {
groupings: Group[] | null;
cursor: string | null;
}): Promise<Group[] | null> => {
const {data} = await context.pack.query(PRODUCT_GROUPINGS_QUERY, {
variables: {after: cursor},
cache: context.storefront.CacheLong(),
});
if (!data?.groups) return null;
const queriedGroupings =
data.groups.edges?.map(({node}: {node: Group}) => node) || [];
const {endCursor, hasNextPage} = data.groups.pageInfo || {};
const compiledGroupings = [...(groupings || []), ...queriedGroupings];
if (hasNextPage) {
return getAllProductGroupings({
groupings: compiledGroupings,
cursor: endCursor,
});
}
return compiledGroupings;
};
const groupings = await getAllProductGroupings({
groupings: null,
cursor: null,
});
return groupings || null;
} catch (error) {
console.error('Error fetching product groupings from Pack:', error);
return null;
}
};
Product pages
On a product page, the Blueprint attaches the product's group in the loader, then builds the options in the component.
In the loader
getProductWithInitialGrouping (in app/lib/server-utils/product.server.ts)
finds the group that contains the product. It then loads every product in that
group from the Storefront API and adds them to the product as initialGrouping.
A product that isn't in a group is returned unchanged. In preview mode, products
that the Storefront API doesn't return, such as drafts, are loaded through the
Shopify Admin API.
app/routes/($locale).products.$handle.tsx
export async function loader({params, context}: Route.LoaderArgs) {
const {handle} = params;
const {storefront} = context;
if (!handle) throw new Response(null, {status: 404});
const [{product: storefrontProduct}, productGroupings] = await Promise.all([
storefront.query(PRODUCT_QUERY, {
variables: {
handle,
selectedOptions: [],
country: storefront.i18n.country,
language: storefront.i18n.language,
},
cache: storefront.CacheShort(),
}),
getProductGroupings(context),
]);
if (!storefrontProduct) throw new Response(null, {status: 404});
const product = await getProductWithInitialGrouping({
context,
product: storefrontProduct,
productGroupings,
});
return {product};
}
The Blueprint's full loader also reads the selected options from the URL, loads the product's Pack page, and handles preview and redirects. See the product template in Templates.
In the component
useProductWithGrouping turns initialGrouping into product.grouping:
grouping.optionsmerges the options of every product in the group, so a color that only one product has still appears in the list. When the group has subgroups, theColoroption (or the first option, if the products have noColoroption) also hasgroups: one set of values for the group's own products and one for each subgroup, named after the subgroup's title.grouping.productsByHandleholds each product in the group.grouping.variantMapfinds a variant in any of the products from its selected options.
app/routes/($locale).products.$handle.tsx
export default function ProductRoute() {
const {product: initialProduct} = useLoaderData<{
product: ProductWithInitialGrouping;
}>();
const product = useProductWithGrouping(initialProduct);
return <Product product={product} />;
}
In the Blueprint's product options, an option value that belongs to another product in the group links to that product's page.
Product cards
Product cards don't load groups themselves. The root loader starts
getProductGroupings without waiting for it and returns the promise as
groupingsPromise. GroupingsProvider (in app/contexts/GroupingsProvider)
reads that promise and keeps the groups in context. It's one of the providers
in the Blueprint's ContextsProvider.
app/root.tsx
export async function loader({context}: Route.LoaderArgs) {
const groupingsPromise = getProductGroupings(context);
return {
groupingsPromise,
};
}
useProductGroupingByHandle(handle) returns the group for a product card's
product. The first time a group is used, the hook loads its products from the
Blueprint's /api/products route and builds the same options and
productsByHandle as on the product page. The group is then stored in context,
so other cards in the same group reuse it.
app/components/ProductItem/ProductItem.tsx
const grouping = useProductGroupingByHandle(
enabledColorSelector && (priority || inView) ? handle : null,
);
The Blueprint's product card only looks up a group when the card shows color
swatches and is on screen. Passing null skips the lookup.
Limits
- The root loader calls
getProductGroupingson every request, and product pages call it again. WithCacheLong, most of these calls are served from Hydrogen's cache rather than Pack. - The Product Groupings field in the Blueprint's Build Your Own Bundle section (see Blueprint sections) is set in the section itself. It doesn't use the groups on this page.