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
button
s all next to each other, - each
button
triggers 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-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 adiv
which is now the parent of thebutton
s.- The
this
part of the attribute's value refers to thediv
element itself. Combined with the fact that it's the parent of thebutton
s, it results in the requests of thebutton
s being "synced" with each other. - The
replace
part of the attribute's value determines the syncing strategy. There are multiple options here. I went withreplace
because 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