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:
| Parameter | Type | Required | Description |
|---|
handle | string | Yes | Collection URL handle |
defaultSort | string | No | Default sort code (uses first configured sort) |
Execute parameters:
| Parameter | Type | Description |
|---|
page | number | Page number (default: 1) |
limit | number | Products per page (default: 24) |
sort | string | Sort option code |
filters | unknown | Filter criteria |
signal | AbortSignal | Per-call abort signal |
includeMeta | boolean | Include _meta in response |
linking | Record<string, unknown> | Dynamic linking parameters |
transformRequest | (body) => body | Custom 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:
| Parameter | Type | Required | Description |
|---|
blockId | string | Yes | Layers block ID |
anchor | string | No | Anchor product/collection ID, Shopify GID, or handle |
Execute parameters:
| Parameter | Type | Description |
|---|
page | number | Page number (default: 1) |
limit | number | Products per page (default: 24) |
filters | unknown | Filter criteria |
signal | AbortSignal | Per-call abort signal |
discounts | DiscountEntitlement[] | Discount entitlements to apply (see below) |
context | BlocksContext | Cart, geo, and custom context (see below) |
linking | Record<string, unknown> | Dynamic linking parameters |
transformRequest | (body) => body | Custom request body transformation |
DiscountEntitlement:
interface DiscountEntitlement {
entitled: {
all?: boolean
products?: string[]
variants?: (string | number)[]
collections?: string[]
}
discount: {
type: 'PERCENTAGE' | 'FIXED_AMOUNT'
value: number
}
}
BlocksContext:
| Property | Type | Description |
|---|
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 |
custom | Record<string, unknown> | Custom context data passed to your block strategy |
client.search() - Full-text search
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:
| Parameter | Type | Description |
|---|
query | string | Search query |
page | number | Page number (default: 1) |
limit | number | Products per page (default: 24) |
filters | unknown | Filter criteria |
signal | AbortSignal | Per-call abort signal |
searchId | string | Cached search ID from prepare() |
temporary | boolean | Don’t persist params for next call |
linking | Record<string, unknown> | Dynamic linking parameters |
transformRequest | (body) => body | Custom request body transformation |
client.suggest() - Predictive search
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:
| Option | Type | Description |
|---|
debounce | number | Debounce delay in ms (default: 300) |
excludeInputQuery | boolean | Remove user’s input from suggestions |
excludeQueries | string[] | Custom strings to filter from suggestions |
signal | AbortSignal | Shared 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:
| Parameter | Type | Required | Description |
|---|
imageId | string | Yes | Image ID from uploadImage() |
page | number | No | Page number (default: 1) |
limit | number | No | Products per page (default: 24) |
filters | unknown | No | Filter criteria |
signal | AbortSignal | No | External abort signal |
linking | Record<string, unknown> | No | Dynamic linking parameters |
transformRequest | (body) => body | No | Custom 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:
| Parameter | Type | Description |
|---|
query | string | Search query (required) |
contentType | string | Filter by content type (e.g., ‘article’) |
page | number | Page number (default: 1) |
limit | number | Results per page (default: 24) |
signal | AbortSignal | Per-call abort signal |
transformRequest | (body) => body | Custom request body transformation |
temporary | boolean | Don’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):
| Parameter | Type | Required | Description |
|---|
product | Product | Yes | A product object from a query result |
selectedOptions | ProductOption[] | No | Pre-selected options (e.g. from URL) |
breakoutOptions | ProductOption[] | No | Breakout options that take precedence over selections |
Reactive state (all ReadonlySignal):
| Signal | Type | Description |
|---|
variants | ProductVariant[] | Variants filtered by breakout options |
selectedVariant | ProductVariant | null | Currently selected variant (null if selection is incomplete) |
options | OptionGroup[] | Option groups with availability status baked in |
images | Image[] | Images filtered and ordered by the selected variant/swatch |
price | PriceData | Current price based on selected variant |
priceRange | PriceRangeData | Price range across all available variants |
carouselPosition | number | Current carousel position (1-based) |
isSelectionComplete | boolean | Whether all required options have been selected |
Actions:
| Method | Description |
|---|
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