Handling Bundle Types
Learn how to detect whether a product is a bundle, identify its type (fixed, dynamic, or bundle of bundles), and route to the appropriate handling logic in your storefront.
Quick Start
Bundle Type Detection
Every product has attributes and metadata that help identify its type and configuration:
import { getByContextProduct } from '@epcc-sdk/sdks-shopper';
// Fetch any product with necessary includes
const response = await getByContextProduct({
path: { product_id: productId },
query: {
include: ['main_image', 'files', 'component_products']
}
});
const product = response.data?.data;
// Check product type using meta.product_types
const productType = product?.meta?.product_types?.[0];
const isBundle = productType === 'bundle';
Bundle Type Identification
| Type | Detection Method | Key Characteristics |
|---|---|---|
| Bundle | meta.product_types[0] === 'bundle' | Contains component products |
| Fixed vs Dynamic | Check component configuration requirements | Fixed: predefined selections, Dynamic: configurable |
| Bundle of Bundles | Component products are also bundles | Navigation container for bundles |
Routing Based on Product Type
Here's how to properly route products based on their type, following the pattern from the Composable Frontend example:
async function ProductPage({ productId }) {
// Fetch product with all necessary includes
const response = await getByContextProduct({
path: { product_id: productId },
query: {
include: ['main_image', 'files', 'component_products']
}
});
const productData = response.data?.data;
if (!productData) return <NotFound />;
// Route based on meta.product_types
switch (productData.meta?.product_types?.[0]) {
case 'standard':
return <DisplayStandardProduct productData={response.data} />;
case 'parent':
// Parent product with variations
return <DisplayVariationProduct productData={response.data} />;
case 'child':
// Child variant - fetch parent for full context
const parentResponse = await getByContextProduct({
path: { product_id: productData.attributes?.base_product_id! },
query: { include: ['main_image', 'files', 'component_products'] }
});
return <DisplayVariationProduct
productData={response.data}
parentProduct={parentResponse.data}
/>;
case 'bundle':
// Bundle product - fetch component images if needed
const componentProducts = response.included?.component_products || [];
const componentImages = await fetchComponentImages(componentProducts);
return <DisplayBundleProduct
productData={response.data}
componentImages={componentImages}
/>;
default:
return <DisplayStandardProduct productData={response.data} />;
}
}
Key Data Structures
Bundle Product Structure
interface BundleProduct {
id: string;
type: "product";
attributes: {
name: string;
sku: string;
// ... other attributes
};
meta: {
product_types: ["bundle"];
bundle_configuration?: {
selected_options: BundleConfiguration["selected_options"];
};
// ... other meta
};
}
// Component product includes bundle configuration
interface ComponentProduct {
id: string;
type: "product";
name: string;
min?: number; // Minimum selections required
max?: number; // Maximum selections allowed
sort_order?: number;
options?: ComponentProductOption[];
}
Component Products Structure
Component products are included when you request them:
// Included in the response when using include=['component_products']
interface ComponentProduct {
id: string;
type: "product";
attributes: {
name: string;
sku: string;
price?: Price[];
commodity_type?: string; // Check for nested bundles
};
}
Bundle Type Characteristics
Fixed Bundles
Fixed bundles have components with predefined selections that cannot be changed:
function FixedBundle({ product, componentProducts }) {
// Fixed bundles have components but no shopper configuration
const hasComponents = componentProducts.length > 0;
const isConfigurable = componentProducts.some(c =>
c.min != null || c.max != null
);
return (
<>
<h1>{product.attributes.name}</h1>
<p>This bundle includes:</p>
{/* Display the included components */}
{hasComponents && componentProducts.map(component => (
<div key={component.id}>
<p>{component.attributes.name} - {component.attributes.sku}</p>
</div>
))}
<AddToCartButton productId={product.id} />
{/* No configuration needed - predefined selections */}
</>
);
}
Dynamic Bundles
Dynamic bundles allow shopper configuration:
function DynamicBundle({ product, componentProducts }) {
const components = product.attributes?.components || {};
const hasRequiredComponents = Object.values(components).some(
comp => comp.min && comp.min > 0
);
return (
<>
<h1>{product.attributes.name}</h1>
{hasRequiredComponents && <p>* Required selections</p>}
<BundleConfigurator
components={components}
componentProducts={componentProducts}
/>
</>
);
}
Bundle of Bundles
Parent bundle containing child bundles:
function BundleOfBundles({ product, childBundles }) {
// Parent bundle is not directly purchasable
const isNavigational = true;
return (
<>
<h1>{product.attributes.name}</h1>
<p>This collection includes the following bundles:</p>
{/* Display child bundle cards in grid */}
{childBundles.map(bundle => (
<BundleCard
key={bundle.id}
bundle={bundle}
link={`/products/${bundle.id}`}
/>
))}
</>
);
}
When to Fetch Additional Data
For Dynamic Bundles
When you detect a dynamic bundle, you need:
- Component products for display
- Component images for visual selection
- Inventory data for availability
if (product.attributes?.components) {
// Fetch component product IDs
const componentIds = await getByContextComponentProductIds({
path: { product_id: product.id }
});
// Component products are already included in initial fetch
// but you may need additional data like images
}
For Bundle of Bundles
Parent bundles need child bundle information:
- Child bundle details
- Individual bundle pricing
- Configuration requirements
if (hasChildBundles) {
// Child bundles are included as component_products
// Each child needs individual configuration
const childConfigs = childBundles.map(bundle => ({
id: bundle.id,
requiresConfiguration: !!bundle.attributes?.components
}));
}
Implementation Patterns
Type-Safe Bundle Handling
// Type guard using meta.product_types
function isBundle(product: Product): boolean {
return product.meta?.product_types?.[0] === 'bundle';
}
// Check for bundle configuration requirements
function hasBundleConfiguration(bundle: Product): boolean {
// Check if bundle has components that need configuration
const components = bundle.included?.component_products || [];
return components.some(component =>
component.min != null || component.max != null
);
}
// Type guard for bundle of bundles
function isBundleOfBundles(
product: Product,
componentProducts: Product[]
): boolean {
if (!isBundle(product)) return false;
return componentProducts.some(
cp => cp.meta?.product_types?.[0] === 'bundle'
);
}
// Usage
const productType = product.meta?.product_types?.[0];
switch (productType) {
case 'bundle':
if (hasBundleConfiguration(product)) {
// Handle dynamic bundle with configuration
} else if (isBundleOfBundles(product, componentProducts)) {
// Handle bundle of bundles
} else {
// Handle fixed bundle
}
break;
// ... other cases
}
Bundle Router Component
export function BundleRouter({ product, componentProducts }) {
// Determine bundle type and route accordingly
const bundleType = useMemo(() => {
if (!isBundle(product)) return 'standard';
if (isDynamicBundle(product)) return 'dynamic';
if (isBundleOfBundles(product, componentProducts)) return 'collection';
return 'fixed';
}, [product, componentProducts]);
const components = {
standard: StandardProduct,
fixed: FixedBundle,
dynamic: DynamicBundle,
collection: BundleCollection
};
const Component = components[bundleType];
return (
<Component
product={product}
componentProducts={componentProducts}
/>
);
}
Key Points to Remember
- Product type detection uses
meta.product_types[0] - Bundle products have type value of
'bundle' - Bundle configuration is determined by component min/max requirements
- Component products must be included in the API request
- Component images may need separate fetching for visual selection
- Each product type requires different display components
Common Pitfalls
Not Including Component Products
// ❌ Wrong: Missing component products
const response = await getByContextProduct({
path: { product_id: productId }
});
// ✅ Correct: Include component products
const response = await getByContextProduct({
path: { product_id: productId },
query: { include: ['component_products'] }
});
Not Checking Product Type Correctly
// ❌ Wrong: Using old commodity_type field
if (product.commodity_type === 'bundle') {
showBundleDisplay();
}
// ✅ Correct: Use meta.product_types
if (product.meta?.product_types?.[0] === 'bundle') {
showBundleDisplay();
}
Not Handling Bundle of Bundles
// ❌ Wrong: Trying to add parent bundle to cart
if (isBundleOfBundles) {
addToCart(parentBundle.id); // This will fail!
}
// ✅ Correct: Navigate to child bundles
if (isBundleOfBundles) {
showBundleCollection(childBundles);
}
Next Steps
Now that you can detect bundle types:
- For dynamic bundles: Continue to List Bundle Components
- For configuration: See Configure Dynamic Bundles
- For UI patterns: Check Bundle Selection Patterns