The SDK’s reactive state is built on framework-agnostic signals. Every controller exposes three ways to consume state changes — you can pick whichever fits your stack:
controller.subscribe(callback) — built-in, no imports needed
subscribe(signal, callback) — standalone utility for any signal
effect(() => { ... }) — direct signal access for custom reactivity
All examples below use a collection controller, but the same patterns apply to search, blocks, suggest, searchContent, and createProductCard.
Vanilla JavaScript
Use subscribe to drive DOM updates directly:
React
Wrap the SDK’s subscribe in a hook using React’s useSyncExternalStore for tear-free reads:
Usage:
Vue
Use Vue’s ref and onUnmounted to bridge SDK state:
Svelte
Use the subscribe callback to drive Svelte’s reactive $state:
Lit
Use subscribe in connectedCallback / disconnectedCallback:
Alpine.js
Alpine pairs well with the SDK in zero-build Shopify themes — boot the client from a CDN, then use an Alpine component to expose the controller state to your template. Hand the controller’s subscribe callback Alpine’s reactive $data and the DOM updates automatically as the SDK signal emits.
Boot the client once
Load the SDK and Alpine from a CDN, then create the client in a single <script type="module"> block in theme.liquid (or any layout). See Loading from a CDN for alternative CDNs and version pinning.
Collection component
Define an Alpine component that owns a client.collection() controller. The init() lifecycle hook creates the controller, wires subscribe into Alpine’s reactive state, and triggers the initial execute(). destroy() tears the subscription and the controller down so navigating away never leaks signals.
The component reads data.products, data.page, and data.totalPages straight off the SDK state and exposes them as Alpine reactive properties — every signal emit re-renders the matching x-text, x-for, and x-show bindings.
Search component
The same pattern works for search. Reuse client.suggest() and client.search() so you get debouncing, abort handling, and the search_id prepare/execute lifecycle for free.
Store for sharing controllers across components
If multiple Alpine components on the same page need the same controller (a header search box that drives a results grid, for example), wrap the controller in an Alpine store so every component subscribes to the same signal.
Alpine evaluates x-data expressions inside with blocks, so accessing data?.priceRange?.formatted works just like it would in plain JS. Use optional chaining everywhere you read SDK state — the controller emits a state with data: null while the first request is in flight.
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:
Using effect directly
For maximum flexibility, use effect to react to any combination of signals:
ProductCard integration
The createProductCard controller follows the same patterns. Here’s a React example: