Htmx in ecommerce


One of my recent side projects is an e-commerce-like site which has the typical pattern of a basket that users can add to, and then check out to purchase all of the items in their basket.

My goal was to create a proof of concept that I could show to interested parties, and so I wanted to keep things quick and simple. For this purpose, I chose to use Go, templ, and htmx. I’m comfortable with these technologies, and I didn’t really want to use a JS backend via something like Astro, even though it is probably also a great choice.

The majority of the site is static. The server fetches the latest products to display, and then inserts them in the HTML response, which is about the extent of the dynamic content. The basket is one of the more interesting parts of the site, since there is an indicator in the header which should update in real time as the user adds and removes items.

Implementing this with htmx is fairly simple, and we can make things feel nice and snappy. Here’s what the basket indicator might look like to begin with:

templ BasketIndicator(itemCount int) {
    <div>
        <!-- Basket icon -->
        <svg aria-hidden="true">...</svg>
        <div>
            <span class="sr-only">Basket </span>
            <span>{ itemCount }</span>
        </div>
    </div>
}

This is static, so we need some way to update the UI as the user modifies their basket. There are various ways to achieve this with htmx, so let’s look at a few.

Events

An approach that I am becoming more fond of due to its flexibility: utilizing events to trigger htmx updates.

The hx-trigger attribute allows us to specify an event that we want to trigger our htmx action on. This gives us the ability to control when the update occurs from both the client (via emitting an event), and from the server (via the HX-Trigger response header).

We’ll update our template as such:

templ BasketIndicator(itemCount int) {
    <div>
        <!-- Basket icon -->
        <svg aria-hidden="true">...</svg>
        <div>
            <span class="sr-only">Basket </span>
            <span
                hx-get="/basket/count"
                hx-trigger="basket-updated from:body"
            >
                { itemCount }
            </span>
        </div>
    </div>
}

We also need to add an endpoint to our server at GET /basket/count which returns the current basket count.

Then, the only other thing we need to do is set the response header HX-Trigger: basket-updated whenever we make a change to the basket on the server. This event will bubble up to the body, at which point our trigger will fire and we’ll send a GET request to our server to fetch the new basket count.

The main downside of this approach is that it requires an extra call to the server whenever we want to update the basket indicator. We’re already visiting the server when we do stuff like adding an item to the basket, so how about we make use of that to update the basket indicator as part of that response?

Out of band swaps

Using hx-swap-oob can help us in a couple of ways:

  1. It minimizes calls to the server, the new element is returned as part of the response that updated the basket
  2. The UX is better than our event-driven approach, since the user won’t have to wait for a server round-trip for the basket indicator to be updated

If you aren’t familiar with this attribute, it allows you to piggyback updates to elements outside of the area your regular response is targeting. In our case, whenever we update the basket on the server we will use an “out of band” swap to update the basket indicator to show the new item count.

Our template gets updated like so:

templ BasketIndicator(itemCount int) {
    <div>
        <!-- Basket icon -->
        <svg aria-hidden="true">...</svg>
        <div>
            <span class="sr-only">Basket </span>
            @BasketItemCount(itemCount)
        </div>
    </div>
}

templ BasketItemCount(itemCount) {
    <span id="basket-item-count" hx-swap-oob="true">{ itemCount }</span>
}

Our server will make sure to render the BasketItemCount template in the response whenever the basket is modified, passing it the new item count. Htmx will then see the out of band attribute and swap the basket item count element for the new one from the server.

Sync

Imagine a case where the user adds 3 different items to their basket in quick succession. There are 3 requests in flight, but the server is being a bit slow, and they could come back in any order. Each of the requests want to add an item to the basket, but we don’t know in which order this will happen.

If your backend is written well, chances are that you have a mutex of some kind around your basket to avoid race conditions like this. In our example though, let’s imagine that we don’t.

The requests happen to come back to the client in such an order that the last response gives us an incorrect basket item count. Using the hx-swap-oob approach, we’d end up with an incorrect count shown on our indicator. If we combine both the out of band and the event-driven approaches, we will (after a short delay) end up with the correct count displayed:

  1. The user clicks on the 3 buttons, the requests are all sent to the server
  2. The requests resolve out-of-order, each triggering the out of band swap
  3. Each of the responses also included an HX-Trigger header that triggered the hx-get of the item count, causing 3 more requests to the /basket/count endpoint
  4. The server responds with the correct item count on at least the last /basket/count call, since the final basket addition had been done by that point

This improves things, but it is a bit wasteful to trigger several calls to the basket count endpoint if we only really care about the latest request that we sent.

Our solution to this is the hx-sync attribute, which gives us more control over how requests are managed when there are multiple of the same request in flight. By default setting this attribute will simply ignore any requests that are already in flight if a newer one is triggered. We can change the value of the attribute to replace to cause our in flight requests to abort and be replaced with our newer one. This is particularly handy if your server is doing a lot of work on these requests, as the server can tell that the request was aborted and drop any further work it would have had to do.

Putting it all together, we get the following:

templ BasketIndicator(itemCount int) {
    <div>
        <!-- Basket icon -->
        <svg aria-hidden="true">...</svg>
        <div>
            <span class="sr-only">Basket </span>
            @BasketItemCount(itemCount)
        </div>
    </div>
}

templ BasketItemCount(itemCount) {
    <span
        id="basket-item-count"
        hx-get="/basket/count"
        hx-trigger="basked-updated from:body"
        hx-swap-oob="true"
        hx-sync="replace"
    >
        { itemCount }
    </span>
}

Overall this gives us:

  • The ability to swap in a new item count from the server via an out of band swap
  • The ability to trigger a call to fetch the latest item count from the server or the client via an event
  • Requests to /basket/count are aborted if a newer one is triggered while one is in flight

This is a bit of an overkill example though, most of the time an out of band swap is probably the simplest and best UX.