ProductList
Description
Displays an array of products in grid or list layout. Includes loading skeleton, empty state, and customizable rendering. Supports responsive columns and add to cart functionality.
Import
import { ProductList } from '@components/frontStore/catalog/ProductList';
Usage
import { ProductList } from '@components/frontStore/catalog/ProductList';
function CategoryPage({ products }) {
return (
<ProductList
products={products}
layout="grid"
gridColumns={4}
/>
);
}
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| products | ProductData[] | Yes | [] | Array of products to display |
| imageWidth | number | No | 800 (grid) / 320 (list) | Base target width in pixels. The height is derived, not passed — see below. |
| isLoading | boolean | No | false | Show loading skeleton |
| emptyMessage | string | ReactNode | No | 'No products found' | Empty state message |
| className | string | No | '' | Additional CSS classes |
| layout | 'grid' | 'list' | No | 'grid' | Display layout mode |
| gridColumns | number | No | 4 | Number of columns (1-6) |
| showAddToCart | boolean | No | false | Show add to cart buttons |
| customAddToCartRenderer | (product) => ReactNode | No | - | Custom add to cart renderer |
| renderItem | (product) => ReactNode | No | - | Custom product item renderer |
Image Sizing
ProductList takes no imageHeight prop. The height comes from the store's configured original product image dimensions (the admin Catalog setting, surfaced to the client as config.catalog.imageDimensions), so every placement renders at the store's true aspect ratio:
const catalogDimensions = useCatalogImageDimensions();
const baseWidth = imageWidth ?? (layout === 'list' ? 320 : 800);
const { width, height } = deriveProductImageSize(baseWidth, catalogDimensions);
Two consequences worth knowing:
- The defaults are ~2× the CSS display width, deliberately — DPR-2 / retina screens stay sharp and the
<Image>srcsetcovers the rest.800for grid,320for list. They are not the rendered pixel size. deriveProductImageSizeclamps the requested width to the store's original width, so it never asks for an upscale. When no usable original is configured it falls back to a square box at the requested width (the fallback original is1200 × 1200).
Pass imageWidth only when a placement genuinely needs a different resolution ceiling — a narrow sidebar shelf, for example. To change the aspect ratio for the whole store, change the Catalog image setting rather than the component.
imageHeight no longer exists on ProductList. Passing it is a no-op: React drops the unknown prop and the height is derived regardless. If your theme still passes imageWidth/imageHeight as a matched square pair, drop the height and let the store's aspect ratio drive it.
Examples
Basic Grid Layout
import { ProductList } from '@components/frontStore/catalog/ProductList';
function Products({ products }) {
return (
<ProductList
products={products}
layout="grid"
gridColumns={3}
/>
);
}
List Layout
import { ProductList } from '@components/frontStore/catalog/ProductList';
function SearchResults({ products }) {
return (
<ProductList
products={products}
layout="list"
/>
);
}
The list layout already defaults imageWidth to 320. Override it only to change the resolution ceiling — the height still follows the store's aspect ratio.
With Loading State
import { ProductList } from '@components/frontStore/catalog/ProductList';
import { useState, useEffect } from 'react';
function CategoryPage() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProducts().then(data => {
setProducts(data);
setLoading(false);
});
}, []);
return (
<ProductList
products={products}
isLoading={loading}
gridColumns={4}
/>
);
}
With Add to Cart
import { ProductList } from '@components/frontStore/catalog/ProductList';
function ShopPage({ products }) {
return (
<ProductList
products={products}
showAddToCart={true}
gridColumns={4}
/>
);
}
Custom Empty Message
import { ProductList } from '@components/frontStore/catalog/ProductList';
function SearchResults({ products, searchTerm }) {
return (
<ProductList
products={products}
emptyMessage={
<div>
<p>No results found for "{searchTerm}"</p>
<a href="/products">Browse all products</a>
</div>
}
/>
);
}
Custom Product Renderer
import { ProductList } from '@components/frontStore/catalog/ProductList';
import { Image } from '@components/common/Image';
function FeaturedProducts({ products }) {
return (
<ProductList
products={products}
gridColumns={3}
renderItem={(product) => (
<div className="featured-product">
<div className="badge">Featured</div>
{product.image && (
<Image
src={product.image.url}
alt={product.name}
width={300}
height={300}
/>
)}
<h3>{product.name}</h3>
<p className="price">{product.price.regular.text}</p>
<a href={product.url}>View Details</a>
</div>
)}
/>
);
}
Custom Add to Cart Button
import { ProductList } from '@components/frontStore/catalog/ProductList';
import { AddToCart } from '@components/frontStore/cart/AddToCart';
function ProductGrid({ products }) {
return (
<ProductList
products={products}
showAddToCart={true}
customAddToCartRenderer={(product) => (
<AddToCart
product={{ sku: product.sku, isInStock: product.inventory.isInStock }}
qty={1}
>
{/* children is (state, actions) => ReactNode — two arguments,
not one destructured object. */}
{(state, actions) => (
<button
onClick={actions.addToCart}
disabled={!state.canAddToCart}
className="custom-add-btn"
>
{state.isLoading ? 'Adding...' : 'Quick Add'}
</button>
)}
</AddToCart>
)}
/>
);
}