Understanding htmx

Including htmx by default is one of the main design decisions I've made in Biff. You don't have to use htmx; it's pretty straightforward to set up ClojureScript and React instead. But if htmx is a good fit for your project, you might find it has a pretty high bang-for-buck ratio—that's been my experience at least.

htmwhat?

Before we get into htmx's tradeoffs, let's briefly cover what it is. htmx is a Javascript library that helps you do server-side rendering with snippets of HTML instead of entire pages. To explain what I mean, consider this humble form:

<form action="/set-foo" method="POST">
  <div>Current foo value: 7</div>
  <input type="number" name="foo" />
  <button type="submit">Update</button>
  ...
</form>

In Clojure, you might create a request handler that saves the input data from this form somewhere and then redirects back to the page the user was already on:

(defn set-foo-handler [{:keys [params]}]
  (let [foo (parse-long (:foo params))]
    (save-foo! foo)
    {:status 303
     :headers {"location" "/foo-page"}}))

After the user gets redirected, the entire page they were on will get re-rendered.

With htmx, we can instead re-render just the form instead of the entire page. First we use a couple hx-* attributes that tell htmx to do its thing:

<form hx-post="/set-foo" hx-swap="outerHTML">
  <div>Current foo value: 7</div>
  <input type="number" name="foo" />
  <button type="submit">Update</button>
  ...
</form>

And then we modify set-foo-handler so that instead of redirecting, it renders a new version of the form:

(defn set-foo-handler [{:keys [params]}]
  (let [foo (parse-long (:foo params))]
    (save-foo! foo)
    {:status 200
     :headers {"content-type" "text/html"}
     :body (rum/render-static-markup
            [:form {:hx-post "/set-foo" :hx-swap "outerHTML"}
             [:div "Current foo value: " foo]
             [:input {:type "number" :name "foo"}]
             [:button {:type "submit"} "Update"]
             ;; ...
             ])}))

When you submit the form, htmx will trigger a POST request, as with the normal form at the beginning. The difference is that htmx will take the HTML response and swap it into the current page where the old form used to be. The rest of the page is untouched.

And crucially, this is done in the same style as traditional server-side rendering: you just write request handlers that return HTML; there's not much client-side logic.

Why not just re-render the whole page?

In some cases that’s fine. But in other cases, you don’t want to lose the state that’s already on the page. Suppose you’re building the next hot social network, and you’ve got a page that shows a feed of posts:

Now you need to implement the heart button so that people can heart their favorite posts. However, if your heart button is a plain-old-form that causes the entire page to reload, there are several potentially undesirable consequences:

  • All the posts in the feed will have to be fetched again.
  • The posts that get fetched might be different.
  • The user might lose their scroll position.
  • The user might lose their draft if they were in the middle of typing a post.

If you instead implement the heart button as an htmx form that only reloads the current post—or even just the heart button itself—then hearting a post will be faster and better.


Those are some minimal examples of what htmx can do. A few other important features:

  • You can put hx-post (or hx-get / hx-put / hx-delete) on other elements, like individual inputs, not just forms.
  • You can use hx-trigger to change the event that triggers the request, such as click, change, or load.
  • hx-target lets you specify where the response body should go, using a CSS selector (e.g. #output or closest div).
  • With hx-swap you can change the way the response body is inserted into the DOM, e.g. set it to beforeend to insert something at the end of a list.

htmx takes the standard HTML behavior of "when the user submits a form / clicks a link, send an HTTP request and load a new page" and generalizes it to "when [something happens], send an HTTP request and put the response [somewhere in the DOM]."

How to think about htmx

In a nutshell: you're not "building an htmx app," you're building a server-side rendered app with htmx. Let me expand on that.

Not all of your app's interactions have to go through htmx. If a plain-old-form has good enough UX for a particular interaction, just use that. You don't have to put hx-post on everything. Similarly, it's OK to use some Javascript. If you were building a simple, "traditional" server-side rendered app, there would be nothing wrong with throwing in a little vanilla JS to spruce things up a bit. Adding htmx into the mix doesn't change that.

Standalone JS components also work nicely. I use a rich text editor component in one of my apps, and from htmx's perspective, it behaves just like a regular textarea. The main thing is that the majority of your application logic should be happening on the server. (See Hypermedia-Friendly Scripting for more details.)

So the fundamental tradeoffs of htmx are similar to the tradeoffs of traditional server-side rendering vs. SPAs. Server-side rendering (thin client) simplifies the programming model because you don't have much distributed state, but it has a lower UX ceiling than a SPA (thick client). If server-side rendering is good enough for your app, then doing it that way will likely take less effort than building it as a SPA. But there is a threshold for your app's interaction requirements above which you're better off going with a SPA.

htmx raises the threshold.

Is htmx right for you?

Given all that, an initial question to ask is "how far would you be able to get with traditional server-side rendering—plain old links and forms?" If the answer is "pretty far," htmx might be worth a shot; if the answer is "no way Jose," htmx probably won't change that.

Carson Gross (htmx author) has an essay When should you use hypermedia? that goes into the details of what kinds of interactions can (and can't) be handled well by htmx. He mentions some specific apps near the end:

To give an example of two famous applications that we think could be implemented cleanly in hypermedia, consider Twitter or GMail. Both web applications are text-and-image heavy, with coarse-grain updates and, thus, would be quite amenable to a hypermedia approach.

Two famous examples of web applications that would not be amenable to a hypermedia approach are Google Sheets and Google Maps. Google Sheets can have a large amount of state within and interdependencies between many cells, making it untenable to issue a server request on every cell update. Google Maps, on the other hand, responds rapidly to mouse movements and simply can’t afford a server round trip for every one of them.

Besides all that, I'd also emphasize that there are plenty of apps for which either approach will be just fine. htmx can take you pretty far, but at the same time, building your app as a SPA doesn't mean it instantly becomes a big ball of mud—especially in ClojureScript, where the React wrappers are *chef's kiss*.

In these cases, you're the most important factor.[1] If you're already productive and happy writing SPAs with re-frame or what-have-you, I would probably just stick with that, unless you want to experiment with htmx for the sake of learning something new. I think htmx really shines for people who feel most at home on the backend and for those who are still in the earlier stages of learning web dev.

 

 

Notes

[1] I'm mainly addressing solo developers here, since that's the audience Biff is targeted to. There will of course be other factors to consider if you're in a team context.

Published by Jacob O'Bryant on 25 Sep 2023

Sign up for Biff: The Newsletter
Announcements, blog posts, et cetera et cetera.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.