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
- Yarn
npm install @haus-storefront-react/product-list
yarn add @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 componentsProductList.Item- Item component for a single productProductList.Image- Image component for displaying the product imageProductList.Price- Price component that provides price data via render propProductList.Quantity- Quantity component for handling amountProductList.AddToCart- Component for adding a product to the cartProductList.Pagination- Pagination component for the product list
ProductList.Root
Root component for ProductList. Provides context to all child components.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
searchInputProps | SearchInputProps | No | - | Search/filter input properties |
infinitePagination | boolean | No | true | Enable infinite scroll pagination |
initialPage | number | No | 1 | Initial page number |
productListIdentifier | string | No | - | Unique identifier for the list |
strategies | { paginationStoreStrategy?: PaginationStoreStrategy } | No | - | Optional pagination store strategy |
sortInput | ProductSortHook | No | - | Optional sort input hook |
paginationInput | PaginationHook | No | - | Optional pagination input hook |
children | (context: ProductListContextValue) => ReactNode | No | - | 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
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
product | SearchResult | Yes | - | Product data to display |
children | React.ReactNode | No | - | Child components |
ProductList.Image
Image component for displaying the product image. Must be used within ProductList.Item.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
alt | string | No | product.productName | Alt text for the image |
asChild | boolean | No | false | Use asChild pattern to merge props with child component |
__scopeProductList | Scope | No | - | 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
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | (context: PriceContext) => ReactNode | No | - | 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 stateProductList.Quantity.Increment- Button to increase quantityProductList.Quantity.Decrement- Button to decrease quantityProductList.Quantity.Input- Input field for direct quantity entry
ProductList.Quantity.Root Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
value | number | Yes | - | Current quantity value |
onValueChange | (value: number) => void | Yes | - | Callback function called when quantity changes |
min | number | No | 0 | Minimum allowed quantity |
max | number | No | - | Maximum allowed quantity (unlimited if not specified) |
step | number | No | 1 | Step size for increment/decrement operations |
children | React.ReactNode | No | - | Child components (sub-components) |
asChild | boolean | No | false | Use 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 cartProductList.AddToCart.Quantity- Quantity controls when item is in cart
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
productVariantId | string | Yes | - | The product variant ID |
callbacks | { preAdd?, preAdjust? } | No | - | Optional callbacks for custom logic before operations |
modifyData | (data: Order) => Order | No | - | Optional function to modify order data |
children | React.ReactNode | ((ctx: AddItemToOrderContextValue) => React.ReactNode) | No | - | Render prop or ReactNode |
__scopeProductList | Scope | No | - | 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 (rendersnullwhen infinite)ProductList.Pagination.NextButton– Next page (used in both classic and infinite)ProductList.Pagination.Pages– Wrapper for page links; use withbuildHrefand optionalonPageClick. Rendersnullwhen infinite ortotalPages <= 1.ProductList.Pagination.Pages.PrevLink– Previous linkProductList.Pagination.Pages.PageList– Page numbers + ellipsis (optionallinkClassName,activeLinkClassName,ellipsisClassName)ProductList.Pagination.Pages.NextLink– Next link
ProductList.InfinitePagination (infinite scroll):
ProductList.InfinitePagination.Root– Root container; shares the same pagination context as Pagination.RootProductList.InfinitePagination.Info– Shows “showing X of Y”; accepts render propchildren. Rendersnullwhen not in infinite modeProductList.InfinitePagination.Progress– Shows progress percent; accepts render propchildren. Rendersnullwhen not in infinite modeProductList.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
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children | ReactNode or (context: ProductListPaginationHelpers) => ReactNode | No | - | Compound components or render prop |
ProductList.Pagination.Pages Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
buildHref | (page: number) => string | No | - | Build href for each page (e.g. SEO) |
maxVisiblePages | number | No | 7 | Max page numbers before ellipsis |
onPageClick | (page: number, ctx: { totalPages }) => void | No | - | Called when a page link is clicked |
ProductList.Pagination.Info / Progress / PrevButton / NextButton
| Subcomponent | Scope | Props | Description |
|---|---|---|---|
Info | InfinitePagination | children?: (props: { showing, total }) => ReactNode | Render prop for “showing X of Y”. Renders null when not in infinite mode. |
Progress | InfinitePagination | children?: (props: { percent }) => ReactNode | Render prop for progress percent. Renders null when not in infinite mode. |
PrevButton | Pagination | children?: ReactNode, asChild?: boolean | Previous page button. Renders null when in infinite mode. |
NextButton | Both | children?: ReactNode, asChild?: boolean | Next 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
| Parameter | Type | Required | Description |
|---|---|---|---|
options | ProductListVariables | Yes | Configuration object |
Options Object
interface ProductListVariables {
searchInputProps?: SearchInputProps
infinitePagination?: boolean
initialPage?: number
productListIdentifier?: string
strategies?: {
paginationStoreStrategy?: PaginationStoreStrategy
}
sortInput?: ProductSortHook
paginationInput?: PaginationHook
}
Returns
| Return Value | Type | Description |
|---|---|---|
variables | SearchInput | Current search input variables |
products | SearchResult[] | Array of products in the list |
setProducts | (products: SearchResult[]) => void | Function to update products array |
facetValues | GroupedFacetValues | Grouped facet values for filtering |
setFacetValues | (facetValues: GroupedFacetValues) => void | Function to update facet values |
isLoading | boolean | Loading state flag |
error | Error | null | Error object if request failed |
data | SearchResult | null | Raw search result data |
pagination | IPagination | Pagination state and helpers |
handleSort | (sort: SearchResultSortParameter) => void | Function to handle sort changes |
totalItems | number | undefined | Total number of items found |
usePagination
Hook that manages pagination state and calculations for product lists.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
options | PaginationVariables | Yes | Configuration object |
Options Object
interface PaginationVariables {
infinitePagination?: boolean
take?: number
skip?: number
initialPage?: number
productListIdentifier?: string
}
Returns
| Return Value | Type | Description |
|---|---|---|
pagination | IPagination | Pagination state object |
initialTake | number | Initial take value for search |
initialSkip | number | Initial skip value for search |
calculatePagination | (totalItems: number, products: SearchResult[]) => void | Function to calculate pagination state |
useSort
Hook that manages sorting state and options for product lists.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
options | ProductSortVariables | Yes | Configuration object |
Options Object
interface ProductSortVariables {
initialSort?: SearchResultSortParameter
initialSortOptions?: {
label: string
value: string | SearchResultSortParameter
}[]
defaultSortOrder?: SearchResultSortParameter
onSortOptionChange?: (sortOption: SearchResultSortParameter) => void
productListIdentifier?: string
}
Returns
| Return Value | Type | Description |
|---|---|---|
sortOptions | { label: string; value: string | SearchResultSortParameter }[] | Available sort options |
currentSortValue | SearchResultSortParameter | Current selected sort value |
defaultSortOrder | SearchResultSortParameter | Default sort order |
handleSortOptionChange | (sortOption: SearchResultSortParameter) => void | Function to handle sort option changes |
useProductListProps
Hook that provides pagination helpers for ProductList.Pagination components.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
__scopeProductList | Scope | No | Scope for component context |
Returns
| Return Value | Type | Description |
|---|---|---|
currentPage | number | Current page number |
totalPages | number | Total number of pages |
totalItems | number | Total number of items |
itemsPerPage | number | Number of items per page |
canGoBack | boolean | Whether previous page is available |
canGoForward | boolean | Whether next page is available |
infinitePagination | boolean | Whether infinite pagination is enabled |
loading | boolean | undefined | Loading state |
getNextButtonProps | (props?: ButtonHTMLAttributes) => ButtonHTMLAttributes | Function to get next button props |
getPrevButtonProps | (props?: ButtonHTMLAttributes) => ButtonHTMLAttributes | Function to get previous button props |
getProgressProps | () => { percent: number } | Function to get progress props |
getInfoProps | () => { showing: number; total: number } | Function to get info props |
nextPage | () => void | Function to navigate to next page |
prevPage | () => void | Function to navigate to previous page |
nextState | SearchInput | Next page search input state |
preFetchNextPage | () => void | Function 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
- React
- React Native
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>
)
}
import { FlatList, Text, View } from 'react-native'
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 <Text>Loading...</Text>
if (error) return <Text>Error loading products</Text>
if (!products.length) return <Text>No products found</Text>
return (
<FlatList
data={products}
keyExtractor={(product) => product.productVariantId}
renderItem={({ item: product }) => (
<ProductList.Item product={product}>
<ProductList.Image />
<Text>{product.productName}</Text>
</ProductList.Item>
)}
/>
)
}}
</ProductList.Root>
)
}
Basic Hook Usage
- React
- React Native
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>
)
}
import { FlatList, Text } from 'react-native'
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 <Text>Loading...</Text>
if (error) return <Text>Error loading products</Text>
if (!products.length) return <Text>No products found</Text>
return (
<FlatList
data={products}
keyExtractor={(product) => product.productVariantId}
renderItem={({ item }) => <Text>{item.productName}</Text>}
/>
)
}
Advanced Usage
Complex Component Configuration with Callbacks
- React
- React Native
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>
)
}
import { FlatList, Text, View } from 'react-native'
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 <Text>Loading...</Text>
if (error) return <Text>Error loading products</Text>
if (!products.length) return <Text>No products found</Text>
return (
<>
<FlatList
data={products}
keyExtractor={(product) => product.productVariantId}
renderItem={({ item: product }) => (
<ProductList.Item product={product}>
<ProductList.Image alt={product.productName} />
<Text>{product.productName}</Text>
<ProductList.Price>
{({ price, priceWithTax, currencyCode }) => (
<Price.Root
price={price}
priceWithTax={priceWithTax}
currencyCode={currencyCode}
asChild
>
<View>
<Price.Amount withCurrency />
<Price.Currency />
</View>
</Price.Root>
)}
</ProductList.Price>
<ProductList.AddToCart
productVariantId={product.productVariantId}
callbacks={{
preAdd: async (productVariantId, quantity) => {
if (quantity > 10) {
throw new Error('Cannot add more than 10 items')
}
},
preAdjust: async (orderLineId, 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 && <Text>Error: {error.message}</Text>}
</>
)}
</ProductList.AddToCart>
</ProductList.Item>
)}
/>
<ProductList.InfinitePagination.Root>
<ProductList.InfinitePagination.Info>
{({ showing, total }) => (
<Text>
Showing {showing} of {total}
</Text>
)}
</ProductList.InfinitePagination.Info>
<ProductList.InfinitePagination.NextButton />
<ProductList.InfinitePagination.Progress>
{({ percent }) => (
<View style={{ width: `${percent}%` }} />
)}
</ProductList.InfinitePagination.Progress>
</ProductList.InfinitePagination.Root>
</>
)
}}
</ProductList.Root>
)
}
Conditional Rendering Patterns
- React
- React Native
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>
)
}
import { FlatList, Text } from 'react-native'
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'
>
{() => <Text>Loading...</Text>}
</ProductList.Root>
)
}
if (error) {
return <Text>Error: {error.message}</Text>
}
if (!products.length) {
return <Text>No products found</Text>
}
return (
<FlatList
data={products}
keyExtractor={(product) => product.productVariantId}
renderItem={({ item: product }) => (
<ProductList.Item product={product}>
<ProductList.Image />
<Text>{product.productName}</Text>
</ProductList.Item>
)}
/>
)
}}
</ProductList.Root>
)
}
Made with ❤️ by Haus Tech Team