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.

Overview

This is the SDK equivalent of Rendering facets in Liquid. The pattern is identical — read facet metaobjects in Liquid, then resolve runtime facet values, counts, and ranges from Layers — but the SDK’s client.collection() controller handles the HTTP request, request deduplication, caching, abort signals, and filter-group transformation for you, and exposes results as a reactive signal you can subscribe to. Use this version when you’ve already installed @commerce-blocks/sdk in your theme. If you’re scripting against the storefront API directly with fetch, see the vanilla Liquid + Fetch guide instead.

Prerequisites

  • The Layers Shopify app installed and synced.
  • Facets configured in the Layers dashboard with Enable as Storefront Facet turned on.
  • The SDK installed and a client created in your theme. See SDK installation.
{% comment %} theme.liquid layout {% endcomment %}
{% assign embed_settings = shop.metafields.layers.embed_settings.value %}

<script type="module">
  import { createClient } from 'https://esm.sh/@commerce-blocks/sdk'

  const { data: client } = createClient({
    token: '{{ embed_settings.storefrontApiToken }}',
    // Mirror dashboard-configured sorts and facets so the SDK knows about them
    sorts: [
      {% assign layers_sorts = shop.metaobjects['app--278936322049--sort_order'] | sort: 'order' %}
      {% for sort in layers_sorts %}
        { name: {{ sort.name.value | json }}, code: {{ sort.code.value | json }} }{% unless forloop.last %},{% endunless %}
      {% endfor %}
    ],
    facets: [
      {% assign layers_facets = shop.metaobjects['app--278936322049--facet'] %}
      {% for facet in layers_facets %}
        { name: {{ facet.name.value | json }}, code: {{ facet.code.value | json }} }{% unless forloop.last %},{% endunless %}
      {% endfor %}
    ],
  })

  window.layers = client
</script>
Generating the sorts and facets arrays from Liquid metaobjects keeps your client config in sync with the dashboard — adding, renaming, or hiding a facet in the Layers dashboard updates the storefront on the next render without a theme deploy.

The pattern

1

Read facet metaobjects in Liquid

Render the sidebar shell from metaobjects exactly like the Fetch version. Each <section> carries the facet code so the SDK results can be matched to the right group.
{% assign layers_facets = shop.metaobjects['app--278936322049--facet'] %}

<aside class="filters" data-collection="{{ collection.handle }}">
  <h2>Filter by</h2>
  {% for facet in layers_facets %}
    <section class="filter-group" data-facet-code="{{ facet.code.value }}">
      <h3>{{ facet.name.value }}</h3>
      <div class="filter-values" data-loading="true"></div>
    </section>
  {% endfor %}
</aside>
2

Create a collection controller

client.collection() returns a reactive controller. Its state signal updates every time execute() runs, and the SDK deduplicates identical in-flight requests and caches results in localStorage.
const sidebar = document.querySelector('.filters')
const collection = window.layers.collection({ handle: sidebar.dataset.collection })

collection.subscribe(({ data, isFetching, error }) => {
  if (isFetching) sidebar.dataset.loading = 'true'
  else delete sidebar.dataset.loading

  if (error) {
    console.error('Layers error:', error)
    return
  }
  if (data) renderFacets(data.facets, data.priceRange)
})
The SDK request includes facet counts and ranges by default — there’s no retrieveFacetCount / includeFacetRanges flag to set. data.facets is the value-count map, and numeric ranges are exposed on top-level fields like data.priceRange (when variants.price is in your facets config).
3

Render values, counts, and ranges

The render code looks at the same data-facet-code attributes as the Fetch version.
function renderFacets(facets, priceRange) {
  sidebar.querySelectorAll('[data-facet-code]').forEach((group) => {
    const code = group.dataset.facetCode
    const container = group.querySelector('.filter-values')
    container.removeAttribute('data-loading')

    // Numeric facets — e.g. variants.price comes back as priceRange
    if (code === 'variants.price' && priceRange) {
      const { min, max } = priceRange
      container.innerHTML = `
        <input type="range" name="min" min="${min}" max="${max}" value="${min}" />
        <input type="range" name="max" min="${min}" max="${max}" value="${max}" />
        <output>${min}${max}</output>
      `
      return
    }

    const values = facets?.[code]
    if (!values) {
      group.hidden = true // no available values for this collection
      return
    }

    container.innerHTML = Object.entries(values)
      .map(
        ([value, count]) => `
          <label>
            <input type="checkbox" name="${code}" value="${value}" />
            ${value} <span class="count">(${count})</span>
          </label>
        `,
      )
      .join('')
  })
}
4

Execute the initial load

Trigger the first request. The subscriber above renders the sidebar as soon as data lands.
await collection.execute()

Refreshing counts as filters are applied

Call execute({ filters }) whenever the active selection changes. The SDK reuses the same controller, dedupes against in-flight requests, and updates the state signal — subscribers re-render automatically.
sidebar.addEventListener('change', () => {
  const activeFilters = {}
  sidebar.querySelectorAll('input[type="checkbox"]:checked').forEach((input) => {
    activeFilters[input.name] ??= []
    activeFilters[input.name].push(input.value)
  })

  collection.execute({ filters: activeFilters })
})
If you’ve configured filter aliases on the client (e.g. color → options.color), the SDK resolves them automatically — you can keep the URL- and UI-friendly keys in your form names.

Why the SDK over Fetch

ConcernFetch versionSDK version
Request body shapeHand-rolled filter_groupPlain filters object, optionally with aliases
Counts and rangesOpt in via retrieveFacetCount / includeFacetRangesAlways returned
CachingDIYBuilt-in cache with cacheLifetime and localStorage persistence
DeduplicationDIYBuilt-in RequestCoordinator
Abort handlingDIY AbortControllerBuilt-in per-controller
ReactivityDIYSignal-based subscribe()
If you have any one of these needs — caching, dedup, abort, reactive UI — the SDK saves you the boilerplate. The Fetch version remains useful when you want zero dependencies.

Troubleshooting

data.facets is empty. Confirm the facets you expect are listed in the facets array passed to createClient. The SDK only requests facet codes it knows about. Counts don’t change when filters are applied. Make sure you’re passing the active filters to execute({ filters }) — the SDK doesn’t watch the DOM. Subscribe to the controller state and re-render on every emit. data.priceRange is undefined. priceRange requires { name: 'Price', code: 'variants.price' } in your facets config. See Responses and errors.

See also