Why I start every architecture debate with the merchant’s editing workflow

As someone who builds both Hydrogen headless storefronts and custom Liquid themes, I am often the first person in the room to talk a merchant out of going headless. When a merchandising team relies on the Shopify Theme Editor to launch weekly campaigns, rearrange blocks, and test App Blocks without waiting on a developer sprint, a well-architected Online Store 2.0 Liquid theme is hard to beat.

In my practice, choosing between Liquid and headless comes down to editorial autonomy, third-party app compatibility, integration complexity, and long-term maintenance budget—never vanity hype or revenue cliches.

How I prevent race conditions in the Section Rendering API

Shopify’s Section Rendering API lets a theme fetch pre-rendered HTML fragments from the server when a shopper toggles collection filters, switches product variants, or updates the cart drawer. However, in the wild I frequently find themes that break when a user clicks three filter checkboxes rapidly: responses arrive out of order, or a partial 200 OK payload returns `null` for a requested section ID and wipes out the product grid.

To make partial updates rock-solid, I use a closure-based request loader that aborts in-flight requests via `AbortController`, tracks a monotonic request counter, enforces same-origin safety, and verifies that every requested section returned a valid HTML string before touching the DOM.

CONCEPT / SECTIONS
  1. 01Filter
  2. 02Fetch sections
  3. 03Validate & update
  4. 04URL + announcement
Request sections, validate the response, update the page and history, then announce the result. Retry or navigate normally on failure.
JAVASCRIPT
export function createSectionLoader() {
  let controller;
  let latest = 0;
  return async (pageUrl, sectionIds) => {
    const request = ++latest;
    controller?.abort();
    controller = new AbortController();
    const url = new URL(pageUrl, location.href);
    if (url.origin !== location.origin) throw new Error("Expected same origin");
    url.searchParams.set("sections", sectionIds.join(","));
    const response = await fetch(url, {signal: controller.signal});
    if (!response.ok) throw new Error("Section request failed");
    const sections = await response.json();
    if (request !== latest) return null;
    for (const id of sectionIds) {
      if (typeof sections[id] !== "string") throw new Error("Section unavailable");
    }
    return sections;
  };
}

My race-condition-safe Section Rendering request helper with AbortController cancellation and fragment validation.

How I finish the interaction beyond the network fetch

  1. I set `aria-busy="true"` on the product grid while loading and keep the user’s active filter controls responsive.
  2. I validate all returned HTML fragments before swapping any DOM nodes, and I silently ignore `AbortError` cancellations from superseded clicks.
  3. I update `history.pushState` only after a successful DOM swap, and I wire `popstate` so the browser Back button restores the previous filter state cleanly.
  4. I restore keyboard focus to a logical element if the previously focused node was replaced, then announce the updated product count via an `aria-live` region.
  5. If a network fetch fails, I keep the existing products on screen and fall back gracefully to standard full-page navigation.

Sources & review

Written from my field experience building and auditing Shopify themes. Unverified GMV or speed comparisons remain excluded; the Section Rendering loader below demonstrates request-layer cancellation and validation.

Content reviewed 2026-10-25. Written in the first person by Mifan Twan Ardana. No production client benchmark is claimed.