Draft: understanding htmx

Next up on the content library roadmap is an essay which I’m calling “understanding htmx.” I’m going to experiment with drafting the essay in sections here on the forum. Feedback and ideas are welcome. I’ll keep the entire draft in this post and add separate replies listing the edits I make.


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 set-foo 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 plain old 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, than building your app 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 just for the sake of learning something new. I think htmx really shines for people who feel most at home on the backend (:person_raising_hand:) and also 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.

1 Like

[edit: moved the content of this post into the OP]

That’s it for tonight. A rough outline for the remainder:

Mental model

  • you’re not building an htmx app, you’re building a server-side rendered app with htmx.
  • Server-side rendering (thin client) simplifies the programming model but puts a ceiling on the user experience. SPAs (thick client) are more complex (distributed state) but have a higher UX ceiling.
  • If server-side rendering is good enough for your app, that approach will tend to take less effort than building a SPA.
  • htmx expands the number of cases for which server-side rendering is good enough

Tips for learning htmx

  • Start without htmx—just do regular server-side rendering. If a plain-old-form + full page reload is fine, just use that. no need to do hx-post every time you make a button.
  • When you hit limitations, then think about how htmx might help. Take a look at the examples.
  • It’s fine to use some javascript! expected, even! htmx is just meant to reduce the amount of javascript you have to write; not eliminate it.
  • htmx goes especially well with drop-in JS components, like this one.
  • After you’ve been doing htmx for at least a little while, I hear alpine.js pairs well with it. alpine lets you inject some reactivity into your server-side rendered html. (that being said, I’ve gotten pretty far myself with just htmx and a bit of custom JS + hyperscript.)
  • You can even try writing your own custom JS to do html snippet fetching/swapping instead of using htmx, as a learning exercise if nothing else

Should you use htmx (or rather, server-side rendering with htmx) for your app?

  • if you get to the point where you’re starting to store state in javascript instead of leaving it in the DOM, that’s a good sign you might be ready for a SPA.
  • If there are a bunch of dependencies between different parts of the page (like a spreadsheet) you probably need a spa
  • complex forms (like in an enterprise saas app) might also be a pain in htmx (?)
  • if you like to spend most of your time in the backend and don’t consider yourself to be much of a frontend dev (:person_raising_hand:), you’ll probably love htmx
  • if you’re already productive and happy writing SPAs, might as well stick with that—you probably aren’t missing anything

Beyond the scope of serious evaluation for any individual frontend project I think there’s definitely merit in reflecting on how the industry can push the limits of “hypermedia” forward. HTMX is particularly attractive to me because of that promise. No need to cram that into your article though :slight_smile:

1 Like

I think something along those lines would be worth mentioning! e.g. about how there are other approaches to modern server-side rendering (hotwire, phoenix live view, I think there’s something in php land too…); I.e. there is some larger industry momentum in this direction. biff’s also not the only framework to do thin client by default, there’s also a little framework called Rails :wink:

Fly.io also is really interesting for the possibilities it opens up here–deploy on the edge without having to write your app as a bunch of serverless js functions

That’s a good one! I think I approached this from the other way at first and over-complicated things a bit.

1 Like

Added this to the end of htmxwhat?:

That’s a minimal example 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].”

Though maybe I should put that at the end of Why not just re-render the whole page? instead…

1 Like

Moved the entire draft into the OP so it isn’t split across multiple posts, and started writing up another section.

Just finished the how to think about htmx section and finished up with an is htmx right for you? section. The draft is now complete. I’ll probably let it marinate for a bit, making any edits that come to mind, and then publish it sometime next week.

2 Likes