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:
- a list of
buttons all next to each other, - each
buttontriggers its own request, but - the response of each request targets the same element.
<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:
- User clicks Button A.
- Request A is made.
- User clicks Button B (before the Response A is complete).
- Request B is made.
- Response A takes too long.
- Response B completes. Content B is shown.
- 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:
hx-synccan be inherited, which means we can declare it on a parent so it affects all of its children. So we put it on adivwhich is now the parent of thebuttons.- The
thispart of the attribute's value refers to thedivelement itself. Combined with the fact that it's the parent of thebuttons, it results in the requests of thebuttons being "synced" with each other. - The
replacepart of the attribute's value determines the syncing strategy. There are multiple options here. I went withreplacebecause it cancels any existing in-flight request when a new request is made.
With this, the improved flow goes like:
- User clicks Button A.
- Request A is made.
- User clicks Button B (before the Response A is complete).
- Request A is cancelled. Response A will never complete.
- Request B is made.
- 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