Skip to main content

Product List

A headless, flexible product list component for e-commerce storefronts. Supports pagination, infinite scroll, add-to-cart, price display, and more.

Purpose

ProductList is a headless component for displaying and managing product lists. It provides context, pagination, price, quantity, and add-to-cart logic. Use it when you need a flexible, composable product listing that handles search, filtering, sorting, and cart operations.

Features

  • Product search and filtering
  • Sorting by name, price, popularity
  • Pagination with multiple strategies (infinite scroll and traditional pagination)
  • Add to cart functionality with pre-operation callbacks
  • Price display with campaign/ordinary price support
  • Quantity management for cart items
  • Loading and error states
  • Event bus integration for cross-component communication

Installation

npm install @haus-storefront-react/product-list

Note: This is not a public package. Contact the Haus Tech Team for access.

API Reference

ProductList

Compound component for displaying and managing product lists.

Sub-components:

  • ProductList.Root - Root component that provides context to all child components
  • ProductList.Item - Item component for a single product
  • ProductList.Image - Image component for displaying the product image
  • ProductList.Price - Price component that provides price data via render prop
  • ProductList.Quantity - Quantity component for handling amount
  • ProductList.AddToCart - Component for adding a product to the cart
  • ProductList.Pagination - Pagination component for the product list

ProductList.Root

Root component for ProductList. Provides context to all child components.

Props

PropTypeRequiredDefaultDescription
searchInputPropsSearchInputPropsNo-Search/filter input properties
infinitePaginationbooleanNotrueEnable infinite scroll pagination
initialPagenumberNo1Initial page number
productListIdentifierstringNo-Unique identifier for the list
strategies{ paginationStoreStrategy?: PaginationStoreStrategy }No-Optional pagination store strategy
sortInputProductSortHookNo-Optional sort input hook
paginationInputPaginationHookNo-Optional pagination input hook
children(context: ProductListContextValue) => ReactNodeNo-Render prop with product list context

ProductListContextValue:

{
variables: SearchInput
products: SearchResult[]
setProducts: (products: SearchResult[]) => void
facetValues: GroupedFacetValues
setFacetValues: (facetValues: GroupedFacetValues) => void
isLoading: boolean
error: Error | null
data: SearchResult | null
pagination: IPagination
handleSort: (sort: SearchResultSortParameter) => void
totalItems: number | undefined
}

ProductList.Item

Item component for a single product. Must be used within ProductList.Root.

Props

PropTypeRequiredDefaultDescription
productSearchResultYes-Product data to display
childrenReact.ReactNodeNo-Child components

ProductList.Image

Image component for displaying the product image. Must be used within ProductList.Item.

Props

PropTypeRequiredDefaultDescription
altstringNoproduct.productNameAlt text for the image
asChildbooleanNofalseUse asChild pattern to merge props with child component
__scopeProductListScopeNo-Scope for component context

Accepts all WebImageProps (standard image element props).

ProductList.Price

Price component for a product. Must be used within ProductList.Item. Provides price data via render prop.

Props

PropTypeRequiredDefaultDescription
children(context: PriceContext) => ReactNodeNo-Render prop that receives price data

PriceContext:

{
price: Price
priceWithTax: Price
currencyCode: CurrencyCode
isFromPrice: boolean
}

ProductList.Quantity

Quantity component for handling amount. Must be used within ProductList.Item.

Sub-components:

  • ProductList.Quantity.Root - Root container that manages quantity state
  • ProductList.Quantity.Increment - Button to increase quantity
  • ProductList.Quantity.Decrement - Button to decrease quantity
  • ProductList.Quantity.Input - Input field for direct quantity entry

ProductList.Quantity.Root Props

PropTypeRequiredDefaultDescription
valuenumberYes-Current quantity value
onValueChange(value: number) => voidYes-Callback function called when quantity changes
minnumberNo0Minimum allowed quantity
maxnumberNo-Maximum allowed quantity (unlimited if not specified)
stepnumberNo1Step size for increment/decrement operations
childrenReact.ReactNodeNo-Child components (sub-components)
asChildbooleanNofalseUse asChild pattern to merge props with child component

ProductList.Quantity.Increment, ProductList.Quantity.Decrement Props

Extends AsChildProps and WebButtonProps (standard button element props).

ProductList.Quantity.Input Props

Extends WebInputProps (standard input element props).

ProductList.AddToCart

Component for adding a product to the cart. Must be used within ProductList.Item. Provides context for add-to-cart logic with support for pre-operation callbacks.

Sub-components:

  • ProductList.AddToCart.Button - Button to add item to cart
  • ProductList.AddToCart.Quantity - Quantity controls when item is in cart

Props

PropTypeRequiredDefaultDescription
productVariantIdstringYes-The product variant ID
callbacks{ preAdd?, preAdjust? }No-Optional callbacks for custom logic before operations
modifyData(data: Order) => OrderNo-Optional function to modify order data
childrenReact.ReactNode | ((ctx: AddItemToOrderContextValue) => React.ReactNode)No-Render prop or ReactNode
__scopeProductListScopeNo-Scope for component context

Callbacks:

{
preAdd?: (productVariantId: string, quantity: number) => Promise<void>
preAdjust?: (orderLineId: string, quantity: number) => Promise<void>
}

AddItemToOrderContextValue:

{
productVariantId: string
callbacks?: {
preAdd?: (productVariantId: string, quantity: number) => Promise<void>
preAdjust?: (orderLineId: string, quantity: number) => Promise<void>
}
modifyData?: (data: Order) => Order
addItemToOrder: (quantity: number) => Promise<Order>
error: Error | null
isLoading: boolean
getButtonProps: () => {
onClick: () => void
disabled: boolean
'aria-label': string
}
isInCart: boolean | undefined
}

ProductList.Pagination and ProductList.InfinitePagination

Pagination is split into two compound namespaces.

ProductList.Pagination (classic pagination):

  • ProductList.Pagination.Root – Root container (shared context with InfinitePagination)
  • ProductList.Pagination.PrevButton – Previous page (renders null when infinite)
  • ProductList.Pagination.NextButton – Next page (used in both classic and infinite)
  • ProductList.Pagination.Pages – Wrapper for page links; use with buildHref and optional onPageClick. Renders null when infinite or totalPages <= 1.
    • ProductList.Pagination.Pages.PrevLink – Previous link
    • ProductList.Pagination.Pages.PageList – Page numbers + ellipsis (optional linkClassName, activeLinkClassName, ellipsisClassName)
    • ProductList.Pagination.Pages.NextLink – Next link

ProductList.InfinitePagination (infinite scroll):

  • ProductList.InfinitePagination.Root – Root container; shares the same pagination context as Pagination.Root
  • ProductList.InfinitePagination.Info – Shows “showing X of Y”; accepts render prop children. Renders null when not in infinite mode
  • ProductList.InfinitePagination.Progress – Shows progress percent; accepts render prop children. Renders null when not in infinite mode
  • ProductList.InfinitePagination.NextButton – Next page / load more button; shared implementation with Pagination.NextButton

Compound usage (classic):

<ProductList.Pagination.Root>
{({ infinitePagination, totalPages }) =>
!infinitePagination && totalPages > 1 && (
<ProductList.Pagination.Pages buildHref={(page) => `?pg=${page}`}>
<ProductList.Pagination.Pages.PrevLink />
<ProductList.Pagination.Pages.PageList />
<ProductList.Pagination.Pages.NextLink />
</ProductList.Pagination.Pages>
)
}
</ProductList.Pagination.Root>

Compound usage (infinite):

<ProductList.InfinitePagination.Root>
<ProductList.InfinitePagination.Info>
{({ showing, total }) => <span>Showing {showing} of {total}</span>}
</ProductList.InfinitePagination.Info>
<ProductList.InfinitePagination.Progress />
<ProductList.InfinitePagination.NextButton>Load more</ProductList.InfinitePagination.NextButton>
</ProductList.InfinitePagination.Root>

ProductList.Pagination.Root / InfinitePagination.Root Props

PropTypeRequiredDefaultDescription
childrenReactNode or (context: ProductListPaginationHelpers) => ReactNodeNo-Compound components or render prop

ProductList.Pagination.Pages Props

PropTypeRequiredDefaultDescription
buildHref(page: number) => stringNo-Build href for each page (e.g. SEO)
maxVisiblePagesnumberNo7Max page numbers before ellipsis
onPageClick(page: number, ctx: { totalPages }) => voidNo-Called when a page link is clicked

ProductList.Pagination.Info / Progress / PrevButton / NextButton

SubcomponentScopePropsDescription
InfoInfinitePaginationchildren?: (props: { showing, total }) => ReactNodeRender prop for “showing X of Y”. Renders null when not in infinite mode.
ProgressInfinitePaginationchildren?: (props: { percent }) => ReactNodeRender prop for progress percent. Renders null when not in infinite mode.
PrevButtonPaginationchildren?: ReactNode, asChild?: booleanPrevious page button. Renders null when in infinite mode.
NextButtonBothchildren?: ReactNode, asChild?: booleanNext page / load more button.

usePaginationContext

Hook that returns pagination context inside ProductList.Pagination.Root or ProductList.InfinitePagination.Root. Use for custom components or typing.

const { currentPage, totalPages, goToPage, getInfoProps } = usePaginationContext()

Returns: ProductListPaginationHelpers

useProductList

Hook that manages product list state, search, pagination, and filtering. Returns product list context value.

Parameters

ParameterTypeRequiredDescription
optionsProductListVariablesYesConfiguration object

Options Object

interface ProductListVariables {
searchInputProps?: SearchInputProps
infinitePagination?: boolean
initialPage?: number
productListIdentifier?: string
strategies?: {
paginationStoreStrategy?: PaginationStoreStrategy
}
sortInput?: ProductSortHook
paginationInput?: PaginationHook
}

Returns

Return ValueTypeDescription
variablesSearchInputCurrent search input variables
productsSearchResult[]Array of products in the list
setProducts(products: SearchResult[]) => voidFunction to update products array
facetValuesGroupedFacetValuesGrouped facet values for filtering
setFacetValues(facetValues: GroupedFacetValues) => voidFunction to update facet values
isLoadingbooleanLoading state flag
errorError | nullError object if request failed
dataSearchResult | nullRaw search result data
paginationIPaginationPagination state and helpers
handleSort(sort: SearchResultSortParameter) => voidFunction to handle sort changes
totalItemsnumber | undefinedTotal number of items found

usePagination

Hook that manages pagination state and calculations for product lists.

Parameters

ParameterTypeRequiredDescription
optionsPaginationVariablesYesConfiguration object

Options Object

interface PaginationVariables {
infinitePagination?: boolean
take?: number
skip?: number
initialPage?: number
productListIdentifier?: string
}

Returns

Return ValueTypeDescription
paginationIPaginationPagination state object
initialTakenumberInitial take value for search
initialSkipnumberInitial skip value for search
calculatePagination(totalItems: number, products: SearchResult[]) => voidFunction to calculate pagination state

useSort

Hook that manages sorting state and options for product lists.

Parameters

ParameterTypeRequiredDescription
optionsProductSortVariablesYesConfiguration object

Options Object

interface ProductSortVariables {
initialSort?: SearchResultSortParameter
initialSortOptions?: {
label: string
value: string | SearchResultSortParameter
}[]
defaultSortOrder?: SearchResultSortParameter
onSortOptionChange?: (sortOption: SearchResultSortParameter) => void
productListIdentifier?: string
}

Returns

Return ValueTypeDescription
sortOptions{ label: string; value: string | SearchResultSortParameter }[]Available sort options
currentSortValueSearchResultSortParameterCurrent selected sort value
defaultSortOrderSearchResultSortParameterDefault sort order
handleSortOptionChange(sortOption: SearchResultSortParameter) => voidFunction to handle sort option changes

useProductListProps

Hook that provides pagination helpers for ProductList.Pagination components.

Parameters

ParameterTypeRequiredDescription
__scopeProductListScopeNoScope for component context

Returns

Return ValueTypeDescription
currentPagenumberCurrent page number
totalPagesnumberTotal number of pages
totalItemsnumberTotal number of items
itemsPerPagenumberNumber of items per page
canGoBackbooleanWhether previous page is available
canGoForwardbooleanWhether next page is available
infinitePaginationbooleanWhether infinite pagination is enabled
loadingboolean | undefinedLoading state
getNextButtonProps(props?: ButtonHTMLAttributes) => ButtonHTMLAttributesFunction to get next button props
getPrevButtonProps(props?: ButtonHTMLAttributes) => ButtonHTMLAttributesFunction to get previous button props
getProgressProps() => { percent: number }Function to get progress props
getInfoProps() => { showing: number; total: number }Function to get info props
nextPage() => voidFunction to navigate to next page
prevPage() => voidFunction to navigate to previous page
nextStateSearchInputNext page search input state
preFetchNextPage() => voidFunction to prefetch next page data

createProductListScope

Creates a scope function for ProductList components to support multiple instances.

Signature

function createProductListScope(): () => { __scopeProductList?: Scope }

Returns

() => { __scopeProductList?: Scope } - A function that returns scope props for ProductList components

Basic Usage

Simple Product List

Loading...
import { ProductList } from '@haus-storefront-react/product-list'

function MyComponent() {
return (
<ProductList.Root
searchInputProps={{
term: '',
groupByProduct: true,
take: 12,
}}
productListIdentifier='main-product-list'
infinitePagination={false}
>
{({ products, isLoading, error }) => {
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error loading products</div>
if (!products.length) return <div>No products found</div>

return (
<ul>
{products.map((product) => (
<li key={product.productVariantId}>
<ProductList.Item product={product}>
<ProductList.Image />
<div>{product.productName}</div>
</ProductList.Item>
</li>
))}
</ul>
)
}}
</ProductList.Root>
)
}

Basic Hook Usage

import { useProductList } from '@haus-storefront-react/product-list'

function MyComponent() {
const { products, isLoading, error, pagination } = useProductList({
searchInputProps: {
term: '',
groupByProduct: true,
take: 12,
},
infinitePagination: false,
productListIdentifier: 'main-product-list',
})

if (isLoading) return <div>Loading...</div>
if (error) return <div>Error loading products</div>
if (!products.length) return <div>No products found</div>

return (
<ul>
{products.map((product) => (
<li key={product.productVariantId}>{product.productName}</li>
))}
</ul>
)
}

Advanced Usage

Complex Component Configuration with Callbacks

import { Price } from '@haus-storefront-react/common-ui'
import { ProductList } from '@haus-storefront-react/product-list'

function MyComponent() {
return (
<ProductList.Root
searchInputProps={{
term: '',
collectionId: undefined,
facetValueIds: undefined,
groupByProduct: true,
sort: undefined,
take: 12,
}}
productListIdentifier='main-product-list'
infinitePagination={false}
>
{({ products, isLoading, error }) => {
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error loading products</div>
if (!products.length) return <div>No products found</div>

return (
<>
<ul>
{products.map((product) => (
<li key={product.productVariantId}>
<ProductList.Item product={product}>
<ProductList.Image alt={product.productName} />
<div>{product.productName}</div>
<ProductList.Price>
{({ price, priceWithTax, currencyCode }) => (
<Price.Root
price={price}
priceWithTax={priceWithTax}
currencyCode={currencyCode}
asChild
>
<div>
<Price.Amount withCurrency />
<Price.Currency />
</div>
</Price.Root>
)}
</ProductList.Price>
<ProductList.AddToCart
productVariantId={product.productVariantId}
callbacks={{
preAdd: async (productVariantId, quantity) => {
// Custom validation before adding item
if (quantity > 10) {
throw new Error('Cannot add more than 10 items')
}
},
preAdjust: async (orderLineId, quantity) => {
// Custom validation before adjusting quantity
if (quantity > 15) {
throw new Error('Maximum quantity is 15')
}
},
}}
>
{({ isInCart, isLoading, error }) => (
<>
{!isInCart && (
<ProductList.AddToCart.Button>
{isLoading ? 'Adding...' : 'Add to cart'}
</ProductList.AddToCart.Button>
)}
{isInCart && (
<ProductList.AddToCart.Quantity.Root
min={1}
max={10}
>
<ProductList.AddToCart.Quantity.Decrement>
-
</ProductList.AddToCart.Quantity.Decrement>
<ProductList.AddToCart.Quantity.Input />
<ProductList.AddToCart.Quantity.Increment>
+
</ProductList.AddToCart.Quantity.Increment>
</ProductList.AddToCart.Quantity.Root>
)}
{error && <div>Error: {error.message}</div>}
</>
)}
</ProductList.AddToCart>
</ProductList.Item>
</li>
))}
</ul>
<ProductList.InfinitePagination.Root>
<ProductList.InfinitePagination.Info>
{({ showing, total }) => (
<span>
Showing {showing} of {total}
</span>
)}
</ProductList.InfinitePagination.Info>
<ProductList.InfinitePagination.NextButton />
<ProductList.InfinitePagination.Progress>
{({ percent }) => <div style={{ width: `${percent}%` }} />}
</ProductList.InfinitePagination.Progress>
</ProductList.InfinitePagination.Root>
</>
)
}}
</ProductList.Root>
)
}

Conditional Rendering Patterns

import { ProductList } from '@haus-storefront-react/product-list'

function ConditionalComponent() {
return (
<ProductList.Root
searchInputProps={{
term: '',
groupByProduct: true,
take: 12,
}}
productListIdentifier='main-product-list'
infinitePagination={false}
>
{({ products, isLoading, error }) => {
if (isLoading) {
return (
<ProductList.Root
searchInputProps={{ term: '', groupByProduct: true, take: 12 }}
productListIdentifier='loading-state'
>
{() => <div>Loading...</div>}
</ProductList.Root>
)
}

if (error) {
return <div>Error: {error.message}</div>
}

if (!products.length) {
return <div>No products found</div>
}

return (
<ul>
{products.map((product) => (
<li key={product.productVariantId}>
<ProductList.Item product={product}>
<ProductList.Image />
<div>{product.productName}</div>
</ProductList.Item>
</li>
))}
</ul>
)
}}
</ProductList.Root>
)
}

Made with ❤️ by Haus Tech Team