Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.uselayers.com/llms.txt

Use this file to discover all available pages before exploring further.

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

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: [{ productId: '123', variantId: '456', quantity: 1 }],
    geo: { country: 'US' },
  },
})

// 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)
contextBlocksContextCart, geo, and custom context (see below)
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
  }
}
BlocksContext:
PropertyTypeDescription
productsInCart{ productId: string, variantId?: string, quantity?: number }[]Products currently in the shopper’s cart
geo{ country?: string, province?: string, city?: string }Geographic context for location-aware recommendations
customRecord<string, unknown>Custom context data passed to your block strategy
Full-text 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
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

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