Skip to main content
This guide covers best practices for SDK setup, controller lifecycle management, and common mistakes to avoid.

SDK initialization

Create client once

Create a single client instance and share it across your application. Creating multiple instances wastes resources and breaks caching.
// ❌ Bad - creates multiple instances
function SearchComponent() {
  const { data: client } = createClient({ 
    token: 'your-token',
    sorts: [{ name: 'Featured', code: 'featured' }],
    facets: [{ name: 'Color', code: 'options.color' }],
  })
  // Creates new client instance on every render
}

// ✅ Good - create once, share globally
const { data: client } = createClient({ 
  token: 'your-token',
  sorts: [{ name: 'Featured', code: 'featured' }],
  facets: [{ name: 'Color', code: 'options.color' }],
})

if (client) {
  function SearchComponent() {
    const search = client.search()
    // ...
  }
}

Required configuration

Always provide sorts and facets when creating the client:
const { data: client, error } = createClient({
  token: 'your-token',
  sorts: [
    { name: 'Featured', code: 'featured' },
    { name: 'Price: Low to High', code: 'price_asc' },
    { name: 'Price: High to Low', code: 'price_desc' },
  ],
  facets: [
    { name: 'Color', code: 'options.color' },
    { name: 'Size', code: 'options.size' },
    { name: 'Vendor', code: 'vendor' },
    { name: 'Product Type', code: 'product_type' },
  ],
})

Controller lifecycle

Always dispose controllers

Controllers maintain subscriptions and state. Dispose them when components unmount to prevent memory leaks.
// ❌ Bad - memory leak
function SearchComponent() {
  const search = client.search()
  // Component unmounts but controller keeps running
}

// ✅ Good - cleanup on unmount
function SearchComponent() {
  const search = client.search()
  
  onUnmount(() => search.dispose()) // framework-specific cleanup
}

Controller reuse

Controllers can be reused for multiple operations:
const search = client.search()

// Use the same controller for multiple searches
await search.execute({ query: 'ring' })
await search.execute({ query: 'necklace' })
await search.execute({ query: 'bracelet' })

// Dispose when done
search.dispose()

Filter handling

Use filter DSL for complex filters

For complex filter conditions, use the filter DSL:
import { filter, and, eq } from '@commerce-blocks/sdk'

// Use filter DSL for explicit control
const collection = client.collection({ handle: 'shirts' })
await collection.execute({ 
  filters: filter(and(
    eq('options.color', 'Red'),
    eq('options.size', 'M')
  ))
})

Error handling

Handle errors gracefully

Always check for errors in the Result object:
const search = client.search()
const result = await search.execute({ query: 'ring' })

if (result.error) {
  switch (result.error._tag) {
    case 'NetworkError':
      showError('Connection issue. Please try again.')
      break
    case 'ApiError':
      if (result.error.status === 429) {
        showError('Too many requests. Please wait a moment.')
      }
      break
    case 'ValidationError':
      showError('Invalid search parameters.')
      break
  }
  return
}

// Safe to use result.data
const products = result.data.products

Prepare failures are non-fatal

If prepare fails, execute will still work (but may be slower):
const search = client.search()
const prepareResult = await search.prepare({ query: 'ring' })

if (prepareResult.error) {
  console.warn('Prepare failed, execute will work but may be slower')
}

// Execute still works
const result = await search.execute({ query: 'ring' })

Next steps

Search Patterns

Learn advanced search patterns including prepare/execute flows.

Caching

Understand SDK caching behavior and optimization.