use javascript for good

2025-11-07

My friend Mike and I were having a conversation about my heart feature where I boldly staked my ground against the use of JavaScript to improve the user experience of "heart'ing" a post. The heart feature is a simple form action that redirects to the post url after clicking it. The "problem" with this is that it has an unsatisfying page bounce.

My original stance on this was to not use any JavaScript because I have stubbornly coupled it with how it can be used for evil and how JavaScript fatigue is a thing because of how complicated frameworks have become. I just have zero desire to fire up NextJS/React, Vue, Svelte (from most evil to less evil), you'll have to pay me money to npx create-next-app.

Mike's proposition is that I should just use JavaScript because it's likely a lot less complex than trying to hack my way around how browsers work. He's right, and to circle back to a previous post I had on software engineer traits I intend to be pragmatic. So I reconsidered my stance and knew the exact tool I'd use to enhance the heart feature, htmx.

htmx isn't a framework, it's a tool to create reactive interactions by supplementing how hypertext works. The core concept is that the state of your application still lives on the server and that an interaction with your page means updating only the relevant parts of your page by having your server respond with the new state in html. Here's a simple and somewhat contrived example:

<button hx-post="/heart/1">
  <strong>1</strong>
</button>

In the above example imagine that 1 is the current count of hearts. The attribute hx-post tells htmx to make an AJAX request to POST /heart/1 when clicked on.

@app.route("/heart/<count>")
def heart(count):
  return "<strong>count + 1</strong>"

The server responds with the updated state and htmx takes the response and swaps out the inner part of it's content, neat huh? Let's look at a real example, the implementation of the heart feature. This blog uses jinja for it's templates so I have the specific heart action in it's own partial.

<!-- partials/heart_action.html -->
<form action="/heart"
      hx-boost="true"
      hx-swap="outerHTML"
      hx-push-url="false"
      hx-target="this">
    <input name="post_id" type="hidden" value="{{ post.slug }}" />
    <button type="submit">&#9829; {{ post.hearts or 0 }}</button>
</form>

<!-- post_detail.html -->
<section class="post">
  {% include "post_content.html" %}
  {% include "partials/heart_action.html" %}
</section>

The original implementation of hearts is still here, it is just a form with a hidden input that passes the field post_id along with the form action request. Instead of using hx-post I'm using hx-boost which basically hijacks the action request and replaces it with an AJAX request instead. This allows the original feature to fallback gracefully and still work even if htmx is not loaded or JavaScript is disabled. htmx will attach Hx headers to the request that allows us to respond appropriately.

@app.route("/heart")
async def heart():
    post_id = request.args.get("post_id")
    ip = get_client_ip() or ""
    hearts.add(post_id, ip)

    # htmx passes Hx-Boosted: true if request was hijacked
    if request.headers.get("Hx-Boosted"):
        post = posts_db.get(post_id)
        return await render_template("partials/heart_action.html", post=post)

    # otherwise redirect to the post and just reload the entire page with new count included
    return redirect(f"/posts/{post_id}")

The heart route will now check if Hx-Boosted is in the header and then respond with the rendered form html which will have the updated heart count. htmx will take the response and swap out the entire form element. This is different than the first example which swapped out the innerHTML. Here we've set hx-swap="outerHTML" which tells htmx to swap out the whole target, in this case hx-target="this" tells htmx to target itself. Then finally we instruct htmx to not push the url state of the form action (the default behavior of forms) with hx-push-url="false".

The user experience of the heart feature is enhanced and still kept the form original behavior with just a few tiny adjustments. htmx is also loaded at the bottom of the page so it doesn't block content if the tiny package (~16kb) never loads. At the end of the day I'm using JavaScript for good and I'll be damned if I haven't been itching for an excuse to use some htmx.

Thanks Mike.