01.software Docs

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.cards

The 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.

SurfaceUse it forPublic stock boundary
listingPage() catalog modePLP, search, and filter gridsReturns product-paginated docs, card-ready cards, and cached availableForSale; no stock, reservedStock, or available quantity fields.
Product detail catalogPDP catalog shell and option selectionReturns display data and catalog availability hints; do not treat it as a live inventory record.
stockSnapshot()Fresh batched variant availability overlaysReturns point-in-time availableStock, availableForSale, and status for requested variant IDs.
stockCheck()Add-to-cart or pre-checkout quantity checksValidates requested quantities against live availability; it is still not a reservation.
CheckoutFinal order creationRechecks sellability and stock before committing the order.
Console/AdminOperational inventory managementOwns 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.

On this page