Skip to main content
All controllers follow the same pattern with reactive state and three ways to subscribe:
  • state — a ReadonlySignal<QueryState<T>> with { data, error, isFetching }
  • execute() — runs the query, returns Result<T, ClientError>
  • subscribe(callback) — reacts to state changes without importing signals
  • dispose() — cleans up subscriptions and aborts pending requests

client.collection() - Browse collections

Browse products in a collection with filters, sort, and pagination.
import { effect, subscribe } from '@commerce-blocks/sdk'

const collection = client.collection({
  handle: 'shirts',
  defaultSort: 'featured', // optional, uses first sort if omitted
})

// Three ways to subscribe to state:

// 1. Controller subscribe (no signal import needed)
const unsubscribe = collection.subscribe(({ data, error, isFetching }) => {
  if (isFetching) showLoading()
  if (error) showError(error.message)
  if (data) render(data.products)
})

// 2. Standalone subscribe (works with any signal)
const unsubscribe2 = subscribe(collection.state, (state) => {
  console.log('State:', state)
})

// 3. Direct signal access (for custom reactivity)
effect(() => {
  const { data } = collection.state.value
  if (data) console.log('Products:', data.products)
})

// Execute queries
await collection.execute() // initial load
await collection.execute({ page: 2 }) // pagination
await collection.execute({ sort: 'price_asc' }) // change sort
await collection.execute({ filters: { color: 'Red' } })
await collection.execute({ includeMeta: true }) // fetch collection metadata

// Cleanup
collection.dispose()
Options:
ParameterTypeRequiredDescription
handlestringYesCollection URL handle
defaultSortstringNoDefault sort code (uses first configured sort)
Execute parameters:
ParameterTypeDescription
pagenumberPage number (default: 1)
limitnumberProducts per page (default: 24)
sortstringSort option code
filtersunknownFilter criteria
signalAbortSignalPer-call abort signal
includeMetabooleanInclude _meta in response
linkingRecord<string, unknown>Dynamic linking parameters
transformRequest(body) => bodyCustom request body transformation
contextContextPer-query market/shopper context. Merges with the global client context. See Context.

client.blocks() - Product recommendations

Product recommendations powered by Layers blocks. Anchored to a product, collection, or cart.
const blocks = client.blocks({
  blockId: 'block-abc123',
  anchor: 'gold-necklace', // product/collection ID or handle
})

await blocks.execute()
await blocks.execute({
  discounts: [
    {
      entitled: { all: true },
      discount: { type: 'PERCENTAGE', value: 10 },
    },
  ],
  context: {
    productsInCart: [{ title: 'Gold Ring', productId: '123', variantId: '456', quantity: 1 }],
    geo: { country: 'US', province: 'CA' },
  },
})

// result.data.block has { id, title, anchor_type, strategy_type, ... }
blocks.dispose()
Options:
ParameterTypeRequiredDescription
blockIdstringYesLayers block ID
anchorstringNoAnchor product/collection ID, Shopify GID, or handle
Execute parameters:
ParameterTypeDescription
pagenumberPage number (default: 1)
limitnumberProducts per page (default: 24)
filtersunknownFilter criteria
signalAbortSignalPer-call abort signal
discountsDiscountEntitlement[]Discount entitlements to apply (see below)
contextContextPer-query market/shopper context. Merges with the global client context. See Context.
linkingRecord<string, unknown>Dynamic linking parameters
transformRequest(body) => bodyCustom request body transformation
DiscountEntitlement:
interface DiscountEntitlement {
  entitled: {
    all?: boolean
    products?: string[]
    variants?: (string | number)[]
    collections?: string[]
  }
  discount: {
    type: 'PERCENTAGE' | 'FIXED_AMOUNT'
    value: number
  }
}
See Context for all available context fields (cart, geo, customer, marketing, and more). Block recommendations commonly use productsInCart, geo, customer, and custom. Product search with facets. Options persist across calls, so subsequent execute() calls merge with existing options.
const search = client.search({ query: 'ring', limit: 20 })

await search.execute()
await search.execute({ page: 2 }) // page persists
await search.execute({ filters: { vendor: 'Nike' } }) // filters update

// Temporary override (doesn't persist for next call)
await search.execute({ query: 'shoes', temporary: true })

// Prepare search (caches searchId for faster execute)
await search.prepare()
await search.execute() // uses cached searchId

search.dispose()
Execute parameters:
ParameterTypeDescription
querystringSearch query
pagenumberPage number (default: 1)
limitnumberProducts per page (default: 24)
filtersunknownFilter criteria
signalAbortSignalPer-call abort signal
searchIdstringCached search ID from prepare()
temporarybooleanDon’t persist params for next call
linkingRecord<string, unknown>Dynamic linking parameters
transformRequest(body) => bodyCustom request body transformation
contextContextPer-query market/shopper context. Merges with the global client context. See Context.
Predictive search with debouncing and local caching. Only full words (trailing space) are cached. Partial input filters cached results client-side.
const suggest = client.suggest({ debounce: 300 })

suggest.subscribe(({ data }) => {
  if (data) renderSuggestions(data.matchedQueries)
})

input.addEventListener('input', (e) => {
  suggest.execute(e.target.value) // debounced automatically
})

suggest.dispose()
Options:
OptionTypeDescription
debouncenumberDebounce delay in ms (default: 300)
excludeInputQuerybooleanRemove user’s input from suggestions
excludeQueriesstring[]Custom strings to filter from suggestions
signalAbortSignalShared abort signal (acts like dispose())

client.uploadImage() - Upload image

Upload an image for image-based search.
const upload = client.uploadImage({ image: file })
upload.subscribe(({ data }) => {
  if (data) console.log('Image ID:', data.imageId)
})

client.searchByImage() - Search by image

Search products using an uploaded image.
const results = client.searchByImage({ imageId: 'uploaded-image-id' })
results.subscribe(({ data }) => {
  if (data) console.log('Similar:', data.products)
})
Parameters:
ParameterTypeRequiredDescription
imageIdstringYesImage ID from uploadImage()
pagenumberNoPage number (default: 1)
limitnumberNoProducts per page (default: 24)
filtersunknownNoFilter criteria
signalAbortSignalNoExternal abort signal
linkingRecord<string, unknown>NoDynamic linking parameters
transformRequest(body) => bodyNoCustom request body transformation
contextContextNoPer-query market/shopper context. Merges with the global client context. See Context.

client.searchContent() - Search articles and blog content

Search articles and blog content with pagination. Options persist across calls, so subsequent execute() calls merge with existing options.
const content = client.searchContent({ query: 'shipping', limit: 10 })

await content.execute()
await content.execute({ page: 2 }) // page persists
await content.execute({ contentType: 'article' }) // filter by content type

// Temporary override (doesn't persist for next call)
await content.execute({ query: 'returns', temporary: true })

content.subscribe(({ data, error, isFetching }) => {
  if (isFetching) showLoading()
  if (error) showError(error.message)
  if (data) renderArticles(data.articles)
})

content.dispose()
Execute parameters:
ParameterTypeDescription
querystringSearch query (required)
contentTypestringFilter by content type (e.g., ‘article’)
pagenumberPage number (default: 1)
limitnumberResults per page (default: 24)
signalAbortSignalPer-call abort signal
transformRequest(body) => bodyCustom request body transformation
contextContextPer-query market/shopper context. Merges with the global client context. See Context.
temporarybooleanDon’t persist params for next call
Response:
interface SearchContentResult {
  articles: Article[]
  query: string
  contentType: string | null
  totalResults: number
  page: number
  resultsPerPage: number
}

interface Article {
  title: string
  handle: string
  summary: string
  author: string
  tags: string[]
  image: ArticleImage | null
  publishedAt: string
}

interface ArticleImage {
  url: string
  width: number
  height: number
}

createProductCard() - Product card controller

Reactive controller for product cards with variant selection and availability logic. All derived values are computed signals that auto-update when inputs change. This is a standalone function, not a client method.
import { createProductCard, effect } from '@commerce-blocks/sdk'

const card = createProductCard({
  product,
  selectedOptions: [{ name: 'Size', value: '7' }],
  breakoutOptions: [{ name: 'Stone', value: 'Ruby' }],
})

// Reactive signals
effect(() => {
  console.log('Variant:', card.selectedVariant.value)
  console.log('Options:', card.options.value) // OptionGroup[]
  console.log('Images:', card.images.value)
  console.log('Price:', card.price.value) // PriceData
})

// Actions
card.selectOption({ name: 'Size', value: 'L' })
card.setSelectedOptions([{ name: 'Size', value: 'L' }]) // merge by name
card.setSelectedVariant(12345) // select by numeric variant ID
card.setCarouselPosition(3) // 1-based manual override

card.subscribe(({ selectedVariant, options, price }) => {
  // Called on any state change
})

card.dispose()
Options (input):
ParameterTypeRequiredDescription
productProductYesA product object from a query result
selectedOptionsProductOption[]NoPre-selected options (e.g. from URL)
breakoutOptionsProductOption[]NoBreakout options that take precedence over selections
Reactive state (all ReadonlySignal):
SignalTypeDescription
variantsProductVariant[]Variants filtered by breakout options
selectedVariantProductVariant | nullCurrently selected variant (null if selection is incomplete)
optionsOptionGroup[]Option groups with availability status baked in
imagesImage[]Images filtered and ordered by the selected variant/swatch
pricePriceDataCurrent price based on selected variant
priceRangePriceRangeDataPrice range across all available variants
carouselPositionnumberCurrent carousel position (1-based)
isSelectionCompletebooleanWhether all required options have been selected
Actions:
MethodDescription
selectOption({ name, value })Select a single option value by name
setSelectedOptions([{ name, value }])Merge multiple option selections by name
setSelectedVariant(variantId)Select a variant directly by numeric ID
setCarouselPosition(position)Set carousel position (1-based manual override)
subscribe(callback)Subscribe to all state changes. Callback receives ProductCardState.
dispose()Clean up subscriptions
ProductCardState (passed to subscribe callback):
interface ProductCardState {
  variants: ProductVariant[]
  selectedVariant: ProductVariant | null
  options: OptionGroup[]
  images: Image[]
  price: PriceData
  priceRange: PriceRangeData
  carouselPosition: number
  isSelectionComplete: boolean
}
Types used by the product card:
interface OptionGroup {
  name: string
  values: OptionValue[]
}

interface OptionValue {
  value: string
  status: 'available' | 'backorderable' | 'sold-out' | 'unavailable'
  selected: boolean
  swatch: Swatch | null
}

interface PriceData {
  price: Price | null
  compareAtPrice: Price | null
  isOnSale: boolean
}

interface PriceRangeData {
  priceRange: PriceRange
  compareAtPriceRange: PriceRange | null
}
OptionValue.status is computed from inventory and availability data across all variants that match the current selection.

Abort signals

Controllers support two levels of abort:
// Shared signal. Cancels everything when component unmounts.
const ac = new AbortController()
const search = client.search({ query: 'ring', signal: ac.signal })

// Per-call signal. Cancels only this request.
const req = new AbortController()
await search.execute({ page: 2, signal: req.signal })

// Either aborting cancels the request (they're linked internally)
ac.abort() // cancels all pending + acts like dispose()
Collection and blocks auto-cancel the previous request when a new execute() starts.

Next steps