F = keyboard shortcuts
A Shower Of Thoughts

Preventing race conditions between sibling htmx elements with `hx-sync`

Published on .

Today I ran into this scenario while building a tabs-like component with htmx:

<button hx-get="/op?action=a" hx-target="[data-result]">Show A</button>
<button hx-get="/op?action=b" hx-target="[data-result]">Show B</button>
<button hx-get="/op?action=c" hx-target="[data-result]">Show C</button>

<section data-result>
<!-- html response from any of the buttons goes here -->
</section>

With this setup it would be possible for this flow to occur:

  1. User clicks Button A.
    1. Request A is made.
  2. User clicks Button B (before the Response A is complete).
    1. Request B is made.
  3. Response A takes too long.
  4. Response B completes. Content B is shown.
  5. Response A completes. Content A is shown (replacing Content B).

Resulting in the content from Button A's response being shown despite the fact that the last click was done on Button B. So, a race condition which would be confusing and frustrating for the user.

Fortunately htmx does provide a way to manage this scenario via hx-sync, although it took me a while to grasp how to use it from its documentation.

How I solved it #

I wrapped the list of buttons on a div element which has the hx-sync attribute set to this:replace:

<div hx-sync="this:replace">
<button hx-get="/op?action=a" hx-target="[data-result]">Show A</button>
<button hx-get="/op?action=b" hx-target="[data-result]">Show B</button>
<button hx-get="/op?action=c" hx-target="[data-result]">Show C</button>
</div>

<section data-result>
<!-- html response from any of the buttons goes here -->
</section>

Let's break it down:

  1. hx-sync can be inherited, which means we can declare it on a parent so it affects all of its children. So we put it on a div which is now the parent of the buttons.
  2. The this part of the attribute's value refers to the div element itself. Combined with the fact that it's the parent of the buttons, it results in the requests of the buttons being "synced" with each other.
  3. The replace part of the attribute's value determines the syncing strategy. There are multiple options here. I went with replace because it cancels any existing in-flight request when a new request is made.

With this, the improved flow goes like:

  1. User clicks Button A.
    1. Request A is made.
  2. User clicks Button B (before the Response A is complete).
    1. Request A is cancelled. Response A will never complete.
    2. Request B is made.
  3. Response B completes. Content B is shown.

Therefore guaranteeing that only the response from the last clicked button (Button B in this example) is always the one shown.

✨✨✨

💌 Reply to this post
More posts related to: development protip htmx