Skip to main content

ProductContext

Description

Provides product page data to child components. Used on product detail pages to access product information, variants, pricing, inventory, and attributes.

Import

import { ProductProvider, useProduct } from '@components/frontStore/catalog/ProductContext';

Usage

Setup Provider

import { ProductProvider } from '@components/frontStore/catalog/ProductContext';

function ProductPage({ product }) {
return (
<ProductProvider product={product}>
{/* Product page components */}
</ProductProvider>
);
}

Access Product Data

import { useProduct } from '@components/frontStore/catalog/ProductContext';

function ProductInfo() {
const product = useProduct();

return (
<div>
<h1>{product.name}</h1>
<p>{product.price.regular.text}</p>
{product.inventory.isInStock ? (
<span>In Stock</span>
) : (
<span>Out of Stock</span>
)}
</div>
);
}

API

ProductProvider Props

NameTypeRequiredDescription
productProductDataYesProduct data object
childrenReactNodeYesChild components

useProduct Hook

Returns the complete ProductData object. Throws error if used outside ProductProvider.

ProductData Interface

FieldTypeDescription
productIdnumberProduct ID
uuidstringProduct UUID
namestringProduct name
descriptionArray<Row>Rich text description
skustringProduct SKU
priceProductPriceDataRegular and special pricing
inventoryobjectStock status
weightobjectWeight value and unit
urlstringProduct URL
imageImageDataMain product image
galleryImageData[]Product gallery images
attributesAttributeIndexItem[]Product attributes
variantGroupVariantGroupProduct variants data

Type Definitions

ProductPriceData

interface ProductPriceData {
regular: {
value: number; // Price value
text: string; // Formatted price
};
special?: {
value: number; // Sale price value
text: string; // Formatted sale price
};
}

VariantGroup

interface VariantGroup {
variantAttributes: Array<{
attributeId: number;
attributeCode: string;
attributeName: string;
options: Array<{
optionId: number;
optionText: string;
productId?: number;
}>;
}>;
items: Array<{
attributes: Array<{
attributeCode: string;
optionId: number;
}>;
product?: {
productId: number;
name: string;
sku: string;
url: string;
price: ProductPriceData;
image?: ImageData;
};
}>;
}

Examples

Product Header

import { useProduct } from '@components/frontStore/catalog/ProductContext';
import { Image } from '@components/common/Image';

function ProductHeader() {
const { name, sku, price, image } = useProduct();

return (
<div className="product-header">
{image && <Image src={image.url} alt={image.alt || name} width={600} height={600} />}
<div>
<h1>{name}</h1>
<p className="sku">SKU: {sku}</p>
<div className="price">
{price.special ? (
<>
<span className="special-price">{price.special.text}</span>
<span className="regular-price line-through">{price.regular.text}</span>
</>
) : (
<span className="regular-price">{price.regular.text}</span>
)}
</div>
</div>
</div>
);
}

Stock Status

import { useProduct } from '@components/frontStore/catalog/ProductContext';

function StockStatus() {
const { inventory } = useProduct();

return (
<div className="stock-status">
{inventory.isInStock ? (
<span className="in-stock">✓ In Stock</span>
) : (
<span className="out-of-stock">Out of Stock</span>
)}
</div>
);
}
import { useProduct } from '@components/frontStore/catalog/ProductContext';
import { Image } from '@components/common/Image';

function ProductGallery() {
const { gallery } = useProduct();

if (!gallery || gallery.length === 0) {
return null;
}

return (
<div className="product-gallery">
{gallery.map((image, index) => (
<Image
key={index}
src={image.url}
alt={image.alt || `Gallery image ${index + 1}`}
width={100}
height={100}
/>
))}
</div>
);
}

Product Attributes

import { useProduct } from '@components/frontStore/catalog/ProductContext';

function ProductAttributes() {
const { attributes } = useProduct();

if (!attributes || attributes.length === 0) {
return null;
}

return (
<div className="product-attributes">
<h3>Specifications</h3>
<dl>
{attributes.map((attr, index) => (
<div key={index}>
<dt>{attr.attributeName}</dt>
<dd>{attr.optionText}</dd>
</div>
))}
</dl>
</div>
);
}

Variant Selector

import { useProduct } from '@components/frontStore/catalog/ProductContext';
import { useState } from 'react';

function VariantSelector() {
const { variantGroup } = useProduct();
const [selectedOptions, setSelectedOptions] = useState<Record<string, number>>({});

if (!variantGroup) {
return null;
}

const handleOptionChange = (attributeCode: string, optionId: number) => {
setSelectedOptions(prev => ({
...prev,
[attributeCode]: optionId
}));
};

return (
<div className="variant-selector">
{variantGroup.variantAttributes.map(attribute => (
<div key={attribute.attributeCode} className="variant-attribute">
<label>{attribute.attributeName}</label>
<div className="options">
{attribute.options.map(option => (
<button
key={option.optionId}
onClick={() => handleOptionChange(attribute.attributeCode, option.optionId)}
className={selectedOptions[attribute.attributeCode] === option.optionId ? 'active' : ''}
>
{option.optionText}
</button>
))}
</div>
</div>
))}
</div>
);
}

Complete Product Page

import { ProductProvider, useProduct } from '@components/frontStore/catalog/ProductContext';
import { Image } from '@components/common/Image';
import { AddToCart } from '@components/frontStore/cart/AddToCart';

function ProductPageContent() {
const product = useProduct();
const { name, sku, description, price, inventory, image, gallery, attributes, weight } = product;

return (
<div className="product-page">
<div className="product-main">
{/* Gallery */}
<div className="product-gallery">
{image && (
<Image src={image.url} alt={image.alt || name} width={600} height={600} />
)}
{gallery && gallery.length > 0 && (
<div className="thumbnails">
{gallery.map((img, i) => (
<Image key={i} src={img.url} alt={img.alt} width={100} height={100} />
))}
</div>
)}
</div>

{/* Product Info */}
<div className="product-info">
<h1>{name}</h1>
<p className="sku">SKU: {sku}</p>

{/* Price */}
<div className="price">
{price.special ? (
<>
<span className="special">{price.special.text}</span>
<span className="regular line-through">{price.regular.text}</span>
</>
) : (
<span className="regular">{price.regular.text}</span>
)}
</div>

{/* Stock */}
<div className="stock">
{inventory.isInStock ? '✓ In Stock' : 'Out of Stock'}
</div>

{/* Add to Cart */}
<AddToCart
product={{ sku, isInStock: inventory.isInStock }}
qty={1}
>
{({ addToCart, isLoading, canAddToCart }) => (
<button
onClick={addToCart}
disabled={!canAddToCart || isLoading}
>
{isLoading ? 'Adding...' : 'Add to Cart'}
</button>
)}
</AddToCart>

{/* Description */}
{description && (
<div className="description">
<h2>Description</h2>
<div>{description}</div>
</div>
)}

{/* Attributes */}
{attributes && attributes.length > 0 && (
<div className="attributes">
<h2>Specifications</h2>
<dl>
{attributes.map((attr, i) => (
<div key={i}>
<dt>{attr.attributeName}</dt>
<dd>{attr.optionText}</dd>
</div>
))}
</dl>
</div>
)}

{/* Weight */}
{weight && (
<p className="weight">
Weight: {weight.value} {weight.unit}
</p>
)}
</div>
</div>
</div>
);
}

export default function ProductPage({ productData }) {
return (
<ProductProvider product={productData}>
<ProductPageContent />
</ProductProvider>
);
}

Features

  • Product Metadata: Name, SKU, description, URL
  • Pricing: Regular and special (sale) prices
  • Inventory: Stock availability status
  • Images: Main image and gallery support
  • Attributes: Product specifications
  • Variants: Multi-attribute product variants
  • Weight: Product weight with unit
  • Extended Fields: Support for custom fields
  • Type Safe: Full TypeScript support
  • Error Handling: Throws error if used outside provider

Usage Notes

  • Only available on product detail pages
  • Must wrap components in ProductProvider
  • The useProduct hook throws error if used outside provider
  • Description is rich text (array of Row objects)
  • Price.special is optional (only present for sale items)
  • Gallery and attributes are optional arrays
  • Extended fields allow custom product data