> ## 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.

# Rendering facets with the SDK

> Render Shopify filter sidebars by combining Layers facet metaobjects in Liquid with the @commerce-blocks/sdk collection controller for live values and counts.

## Overview

This is the SDK equivalent of [Rendering facets in Liquid](/developers/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`](/sdk/installation) in your theme. If you're scripting against the storefront API directly with `fetch`, see the [vanilla Liquid + Fetch guide](/developers/rendering-facets-in-liquid) 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](/sdk/installation).

```liquid theme={null}
{% 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>
```

<Tip>
  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.
</Tip>

## The pattern

<Steps>
  <Step title="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.

    ```liquid theme={null}
    {% 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>
    ```
  </Step>

  <Step title="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`.

    ```js theme={null}
    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).
  </Step>

  <Step title="Render values, counts, and ranges">
    The render code looks at the same `data-facet-code` attributes as the Fetch version.

    ```js theme={null}
    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('')
      })
    }
    ```
  </Step>

  <Step title="Execute the initial load">
    Trigger the first request. The subscriber above renders the sidebar as soon as data lands.

    ```js theme={null}
    await collection.execute()
    ```
  </Step>
</Steps>

## 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.

```js theme={null}
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](/sdk/installation#transforms-and-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

| Concern            | Fetch version                                          | SDK version                                                        |
| :----------------- | :----------------------------------------------------- | :----------------------------------------------------------------- |
| Request body shape | Hand-rolled `filter_group`                             | Plain `filters` object, optionally with aliases                    |
| Counts and ranges  | Opt in via `retrieveFacetCount` / `includeFacetRanges` | Always returned                                                    |
| Caching            | DIY                                                    | Built-in cache with `cacheLifetime` and `localStorage` persistence |
| Deduplication      | DIY                                                    | Built-in `RequestCoordinator`                                      |
| Abort handling     | DIY `AbortController`                                  | Built-in per-controller                                            |
| Reactivity         | DIY                                                    | Signal-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](/sdk/api-reference/responses-and-errors).

## See also

* [Rendering facets in Liquid (Fetch)](/developers/rendering-facets-in-liquid) — the no-dependency equivalent of this guide
* [SDK installation](/sdk/installation) — config reference including `sorts`, `facets`, and `filterAliases`
* [Client methods → `client.collection()`](/sdk/api-reference/client-methods) — full controller API
* [Filtering](/sdk/api-reference/filtering) — filter DSL reference
* [Liquid integration](/developers/liquid-integration) — reading metaobjects in Liquid
