controller.subscribe(callback)— built-in, no imports neededsubscribe(signal, callback)— standalone utility for any signaleffect(() => { ... })— direct signal access for custom reactivity
search, blocks, suggest, searchContent, and createProductCard.
Vanilla JavaScript
Usesubscribe to drive DOM updates directly:
Copy
Ask AI
import { createClient } from '@commerce-blocks/sdk'
const { data: client } = createClient({
token: 'your-token',
sorts: [{ name: 'Featured', code: 'featured' }],
facets: [{ name: 'Color', code: 'options.color' }],
})
const collection = client.collection({ handle: 'shirts' })
const grid = document.getElementById('product-grid')
const loader = document.getElementById('loader')
const unsubscribe = collection.subscribe(({ data, error, isFetching }) => {
loader.hidden = !isFetching
if (error) {
grid.innerHTML = `<p>Error: ${error.message}</p>`
return
}
if (data) {
grid.innerHTML = data.products
.map((p) => `<div class="product-card"><h3>${p.title}</h3></div>`)
.join('')
}
})
await collection.execute()
// Cleanup when done
unsubscribe()
collection.dispose()
React
Wrap the SDK’ssubscribe in a hook using React’s useSyncExternalStore for tear-free reads:
Copy
Ask AI
import { useSyncExternalStore, useMemo, useEffect } from 'react'
import { getClient, type QueryState } from '@commerce-blocks/sdk'
function useLayersSubscription<T>(
createController: () => {
state: { value: QueryState<T> }
subscribe: (cb: (state: QueryState<T>) => void) => () => void
dispose: () => void
},
deps: unknown[] = [],
) {
const controller = useMemo(() => createController(), deps)
useEffect(() => {
return () => controller.dispose()
}, [controller])
return useSyncExternalStore(
(onStoreChange) => controller.subscribe(() => onStoreChange()),
() => controller.state.value,
)
}
Copy
Ask AI
function CollectionGrid({ handle }: { handle: string }) {
const { data: client } = getClient()
const { data, error, isFetching } = useLayersSubscription(
() => client.collection({ handle }),
[handle],
)
if (isFetching) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
if (!data) return null
return (
<div className="product-grid">
{data.products.map((product) => (
<div key={product.id} className="product-card">
<h3>{product.title}</h3>
</div>
))}
</div>
)
}
Vue
Use Vue’sref and onUnmounted to bridge SDK state:
Copy
Ask AI
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { getClient, type CollectionResult, type ClientError } from '@commerce-blocks/sdk'
const props = defineProps<{ handle: string }>()
const products = ref<CollectionResult | null>(null)
const error = ref<ClientError | null>(null)
const isFetching = ref(false)
const { data: client } = getClient()
const collection = client.collection({ handle: props.handle })
const unsubscribe = collection.subscribe((state) => {
products.value = state.data
error.value = state.error
isFetching.value = state.isFetching
})
onMounted(() => {
collection.execute()
})
onUnmounted(() => {
unsubscribe()
collection.dispose()
})
</script>
<template>
<p v-if="isFetching">Loading...</p>
<p v-else-if="error">Error: {{ error.message }}</p>
<div v-else-if="products" class="product-grid">
<div v-for="product in products.products" :key="product.id" class="product-card">
<h3>{{ product.title }}</h3>
</div>
</div>
</template>
Svelte
Use thesubscribe callback to drive Svelte’s reactive $state:
Copy
Ask AI
<script lang="ts">
import { onDestroy } from 'svelte'
import { getClient } from '@commerce-blocks/sdk'
const { handle }: { handle: string } = $props()
const { data: client } = getClient()
const collection = client.collection({ handle })
let products = $state(null)
let error = $state(null)
let isFetching = $state(false)
const unsubscribe = collection.subscribe((state) => {
products = state.data
error = state.error
isFetching = state.isFetching
})
collection.execute()
onDestroy(() => {
unsubscribe()
collection.dispose()
})
</script>
{#if isFetching}
<p>Loading...</p>
{:else if error}
<p>Error: {error.message}</p>
{:else if products}
<div class="product-grid">
{#each products.products as product (product.id)}
<div class="product-card">
<h3>{product.title}</h3>
</div>
{/each}
</div>
{/if}
Lit
Usesubscribe in connectedCallback / disconnectedCallback:
Copy
Ask AI
import { LitElement, html } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import { getClient, type CollectionResult, type ClientError } from '@commerce-blocks/sdk'
@customElement('product-grid')
export class ProductGrid extends LitElement {
@property() handle = ''
@state() private products: CollectionResult | null = null
@state() private error: ClientError | null = null
@state() private isFetching = false
private controller?: ReturnType<ReturnType<typeof getClient>['data']['collection']>
private unsubscribe?: () => void
connectedCallback() {
super.connectedCallback()
const { data: client } = getClient()
this.controller = client.collection({ handle: this.handle })
this.unsubscribe = this.controller.subscribe((state) => {
this.products = state.data
this.error = state.error
this.isFetching = state.isFetching
})
this.controller.execute()
}
disconnectedCallback() {
super.disconnectedCallback()
this.unsubscribe?.()
this.controller?.dispose()
}
render() {
if (this.isFetching) return html`<p>Loading...</p>`
if (this.error) return html`<p>Error: ${this.error.message}</p>`
if (!this.products) return html``
return html`
<div class="product-grid">
${this.products.products.map(
(product) => html`
<div class="product-card">
<h3>${product.title}</h3>
</div>
`,
)}
</div>
`
}
}
Using Standalone subscribe
The SDK exports a subscribe utility that works with any signal, not just controller state. This is useful for watching computed values or composing signals:
Copy
Ask AI
import { subscribe, computed, effect } from '@commerce-blocks/sdk'
const collection = client.collection({ handle: 'shirts' })
// Watch just the product count
const productCount = computed(() => {
const { data } = collection.state.value
return data?.products.length ?? 0
})
const unsubscribe = subscribe(productCount, (count) => {
document.getElementById('count').textContent = `${count} products`
})
Using effect Directly
For maximum flexibility, use effect to react to any combination of signals:
Copy
Ask AI
import { effect } from '@commerce-blocks/sdk'
const search = client.search({ query: 'ring' })
const collection = client.collection({ handle: 'shirts' })
const dispose = effect(() => {
const searchState = search.state.value
const collectionState = collection.state.value
const isAnyLoading = searchState.isFetching || collectionState.isFetching
document.getElementById('global-loader').hidden = !isAnyLoading
})
// Cleanup
dispose()
ProductCard Integration
ThecreateProductCard controller follows the same patterns. Here’s a React example:
Copy
Ask AI
import { useSyncExternalStore, useMemo, useEffect, useRef } from 'react'
import { createProductCard, type ProductCardState } from '@commerce-blocks/sdk'
function getCardSnapshot(card: ReturnType<typeof createProductCard>) {
return {
selectedVariant: card.selectedVariant.value,
options: card.options.value,
price: card.price.value,
images: card.images.value,
}
}
function ProductCard({ product }: { product: Product }) {
const card = useMemo(() => createProductCard({ product }), [product])
const snapshotRef = useRef(getCardSnapshot(card))
useEffect(() => {
return () => card.dispose()
}, [card])
const state = useSyncExternalStore(
(onStoreChange) => card.subscribe(() => {
snapshotRef.current = getCardSnapshot(card)
onStoreChange()
}),
() => snapshotRef.current,
)
return (
<div>
<h3>{product.title}</h3>
{state.options.map((group) => (
<div key={group.name}>
<label>{group.name}</label>
{group.values.map((opt) => (
<button
key={opt.value}
disabled={opt.status === 'unavailable'}
data-selected={opt.selected}
onClick={() => cardRef.current.selectOption({ name: group.name, value: opt.value })}
>
{opt.value}
{opt.status === 'sold-out' && ' (Sold out)'}
</button>
))}
</div>
))}
{state.price.price && <p>{state.price.price.formatted}</p>}
</div>
)
}