Product listing grid
Render a product listing grid with the SDK listingPage helper.
Product listing grid
Render a product listing grid using
createQueryHooks(client).useProductListingPage(). The helper calls the
cacheable public listing endpoint and returns both docs and card-ready
cards.
'use client'
import type { ProductListingCard } from '@01.software/sdk'
import type { ProductListingPageCatalogParams } from '@01.software/sdk'
import { createQueryHooks } from '@01.software/sdk/query'
import { useClient } from '@/lib/sdk'
function getMediaUrl(media: ProductListingCard['primaryImage']) {
return media && typeof media === 'object' && 'url' in media
? (media.url ?? null)
: null
}
export function ProductGrid({
params = {
limit: 24,
search: 'shirt',
filters: {
categoryIds: ['category-1'],
price: { min: 10000, max: 50000 },
availableForSale: true,
},
basePath: '/shop',
},
}: {
params?: ProductListingPageCatalogParams
}) {
const client = useClient()
const query = createQueryHooks(client)
const { data } = query.useProductListingPage(params)
const cards = data?.cards ?? []
return (
<ul>
{cards.map((card) => {
const imageUrl = getMediaUrl(card.primaryImage)
return (
<li key={card.id}>
<a href={card.href}>
{imageUrl ? <img src={imageUrl} alt={card.title} /> : null}
<h3>{card.title}</h3>
<p>
{card.priceRange.isPriceRange
? `${card.priceRange.minPrice}–${card.priceRange.maxPrice}`
: card.priceRange.minPrice}
</p>
</a>
{card.swatches.length > 0 && (
<ul aria-label="Available colors">
{card.swatches.map((swatch) => (
<li key={swatch.optionValueId}>
<a
href={swatch.href}
aria-disabled={!swatch.availableForSale}
style={{ background: swatch.swatchColor ?? undefined }}
>
{swatch.label}
</a>
</li>
))}
</ul>
)}
</li>
)
})}
</ul>
)
}For server prefetch and hydration, use the same query factory on the server and the hook on the client:
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import { productListingPageQueryOptions } from '@01.software/sdk/query'
import { createClient } from '@01.software/sdk'
export async function ProductListingPage() {
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_01_KEY!,
})
const queryClient = new QueryClient()
const params = { limit: 24, basePath: '/shop' }
await queryClient.prefetchQuery(
productListingPageQueryOptions(client.commerce, params),
)
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProductGrid params={params} />
</HydrationBoundary>
)
}For non-React code, call the commerce helper directly:
const page = await client.commerce.product.listingPage({
limit: 24,
sort: '-createdAt',
filters: { tagIds: ['featured'] },
basePath: '/shop',
})
const cards = page.cardsThe card model is intentionally minimal - it derives only what every
listing card needs. For richer data (brand, categories, tags) read
item.product directly. Each swatch href is a hint-only URL
(?opt.<optionId>=<valueId>); the detail page resolves it through
resolveProductSelection(detail, { search }).
Single-group products emit swatches: []. Storefronts that want a chip
even for a single colorway can fall back to item.groups themselves.
Use commerce.product.listingGroupsCatalog({ productIds }) for curated
sections where product IDs are already known. Use listingPage() for PLP
pagination, search, sorting, and public-safe filters.
Listing pagination is product-paginated, not colorway-expanded. If one product has several swatches, those swatches stay inside a single product card.
Public stock boundary
Catalog listing responses are safe to render, but they are not live inventory
records. commerce.product.listingPage() returns listing/card data and cached
availableForSale; it does not expose stock, reservedStock, or available
quantities.
| Surface | Use it for | Public stock boundary |
|---|---|---|
listingPage() catalog mode | PLP, search, and filter grids | Returns product-paginated docs, card-ready cards, and cached availableForSale; no stock, reservedStock, or available quantity fields. |
| Product detail catalog | PDP catalog shell and option selection | Returns display data and catalog availability hints; do not treat it as a live inventory record. |
stockSnapshot() | Fresh batched variant availability overlays | Returns point-in-time availableStock, availableForSale, and status for requested variant IDs. |
stockCheck() | Add-to-cart or pre-checkout quantity checks | Validates requested quantities against live availability; it is still not a reservation. |
| Checkout | Final order creation | Rechecks sellability and stock before committing the order. |
| Console/Admin | Operational inventory management | Owns raw inventory fields such as stock and reservedStock. |
Do not derive a public available quantity from catalog responses. Use
stockSnapshot() for display overlays and stockCheck() or checkout for
quantity-sensitive decisions.
For SSG/ISR and webhook-driven cache invalidation, tag product listing reads
with storefrontCacheResources.productListing(scope); see
Storefront Cache Resources.